forked from orson/bachemap
Compare commits
32 Commits
fine_tunni
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ee49adc084 | |||
| c99a897d92 | |||
| ec1a631737 | |||
| a392f43904 | |||
| c8bcebf647 | |||
| af52eb6e54 | |||
| 8fbd2eba5a | |||
| fd45588e19 | |||
| 999fe39ad4 | |||
| 07bf150f35 | |||
| 1011ad9b84 | |||
| 3e9c07626c | |||
| ac4bb0ec51 | |||
| d4b46fb621 | |||
| 54ad0dcc17 | |||
| 36675072a8 | |||
| e79012673a | |||
| 0e67748aac | |||
| 1e51771917 | |||
| e6e6620eb7 | |||
| ea0b967366 | |||
| f3377eb722 | |||
| 3a903736e4 | |||
| ffcbaff534 | |||
| 849b5cbafc | |||
| 61f970c56d | |||
| 78a98b85fb | |||
| d6b13864a4 | |||
| 7650e67aa5 | |||
| 599cc12a05 | |||
| aeb7ca9749 | |||
| aef9995380 |
263
app.py
263
app.py
@ -1,31 +1,93 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory
|
from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory
|
||||||
|
from flask_pymongo import PyMongo, ObjectId
|
||||||
from flask_login import LoginManager, UserMixin, login_user, login_required, current_user, logout_user
|
from flask_login import LoginManager, UserMixin, login_user, login_required, current_user, logout_user
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from datetime import datetime
|
||||||
|
from flask_pymongo import ObjectId
|
||||||
import os
|
import os
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, FileField, SubmitField, DateTimeField, SelectField, PasswordField
|
||||||
|
from wtforms.validators import DataRequired, Length
|
||||||
import requests
|
import requests
|
||||||
from config import Config
|
from config import Config
|
||||||
from geopy.geocoders import Nominatim
|
from geopy.geocoders import Nominatim
|
||||||
from forms import *
|
|
||||||
from db import mongo
|
|
||||||
|
|
||||||
geolocator = Nominatim(user_agent="Bachemapa @ baches.qro.mx")
|
geolocator = Nominatim(user_agent="Bachemapa @ baches.qro.mx")
|
||||||
|
|
||||||
def create_app(config=Config):
|
def create_app(config=Config):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(config)
|
app.config.from_object(config)
|
||||||
|
|
||||||
mongo.init_app(app)
|
mongo = PyMongo(app)
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
|
login_manager.login_view = 'thelogin'
|
||||||
login_manager.session_protection = "strong"
|
login_manager.session_protection = "strong"
|
||||||
|
|
||||||
|
class User(UserMixin):
|
||||||
|
def __init__(self, user_data):
|
||||||
|
self.id = str(user_data['_id'])
|
||||||
|
self.username = user_data['username']
|
||||||
|
self.referral_code = user_data['referral_code']
|
||||||
|
self.invited_by = user_data.get('invited_by')
|
||||||
|
self.is_admin = user_data.get('is_admin', False)
|
||||||
|
self.pwd = user_data.get('pwd')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get(user_id):
|
||||||
|
user_data = mongo.db.users.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if user_data:
|
||||||
|
return User(user_data)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class PinForm(FlaskForm):
|
||||||
|
description = StringField('¿Qué estamos viendo?', validators=[DataRequired()])
|
||||||
|
photo = FileField('Evidencia fotogénica', validators=[DataRequired()])
|
||||||
|
timedate = DateTimeField(default=datetime.now())
|
||||||
|
typeofpin = SelectField('Tipo de cosa', choices=['bache', 'coladera', 'obra sin terminar', 'escombro', 'robo-asalto', 'biciestacionamiento', 'mala iluminación', 'bici blanca', 'zapato blanco'])
|
||||||
|
|
||||||
|
submit = SubmitField('Agregar')
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField('Usuario', validators=[DataRequired()])
|
||||||
|
pwd = PasswordField('Tu clave', validators=[DataRequired()])
|
||||||
|
submit = SubmitField('Entrar')
|
||||||
|
|
||||||
|
def Unique(model, field, message=None):
|
||||||
|
def _unique(form, field_data):
|
||||||
|
if mongo.db[model.__name__.lower()].find_one({field.name: field_data.data}):
|
||||||
|
raise ValidationError(message or f"{field.name} must be unique.")
|
||||||
|
return _unique
|
||||||
|
|
||||||
|
class RegistrationForm(FlaskForm):
|
||||||
|
username = StringField('Nombre de usuarix', validators=[DataRequired(), Unique('users', StringField('username', message="Este usuario ya existe"))])
|
||||||
|
pwd = PasswordField('Clave', validators=[DataRequired(), Length(min=10), Unique('users', StringField('pwd', message="Esta clave no es muy buena, escoge otra"))])
|
||||||
|
referral = StringField('ID de quien te invito', [DataRequired()])
|
||||||
|
submit = SubmitField('Registrar')
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
||||||
|
|
||||||
@app.route('/', methods=['GET', 'POST'])
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
def index():
|
def index():
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
form = PinForm()
|
form = PinForm()
|
||||||
pins = mongo.db.pins.find()
|
pins = mongo.db.pins.find()
|
||||||
return render_template('index.html', pins=pins, form=form)
|
clean_pins=[]
|
||||||
|
for pin in pins:
|
||||||
|
pin['_id'] = str(pin['_id'])
|
||||||
|
if 'time' in pin and pin['time']:
|
||||||
|
pin['time'] = pin['time'].strftime("%d/%m/%Y %H:%M")
|
||||||
|
if 'last_mod' in pin and pin['last_mod']:
|
||||||
|
pin['last_mod'] = pin['last_mod'].strftime("%d/%m/%Y %H:%M")
|
||||||
|
if 'reviewed' in pin:
|
||||||
|
pin['reviewed'] = str(pin['reviewed'])
|
||||||
|
if 'fixed' in pin:
|
||||||
|
pin['fixed'] = str(pin['fixed'])
|
||||||
|
clean_pins.append(pin)
|
||||||
|
print(type(pins))
|
||||||
|
return render_template('index.html', pins=clean_pins, form=form)
|
||||||
else:
|
else:
|
||||||
form = request.form
|
form = request.form
|
||||||
try:
|
try:
|
||||||
@ -46,7 +108,12 @@ def create_app(config=Config):
|
|||||||
'typeofpin': request.form['typeofpin'],
|
'typeofpin': request.form['typeofpin'],
|
||||||
'added_by': current_user.id,
|
'added_by': current_user.id,
|
||||||
'description': request.form['description'],
|
'description': request.form['description'],
|
||||||
'address': str(geolocator.reverse(str(request.form['lat'])+ ", "+request.form['lng']))
|
'address': str(geolocator.reverse(str(request.form['lat'])+ ", "+request.form['lng'])),
|
||||||
|
'reviewed': True,
|
||||||
|
'fixed': False,
|
||||||
|
'last_mod': datetime.now(),
|
||||||
|
'votes': -1,
|
||||||
|
'removal_reason': None,
|
||||||
}
|
}
|
||||||
print(geolocator.reverse(str(request.form['lat'])+ ", "+request.form['lng']))
|
print(geolocator.reverse(str(request.form['lat'])+ ", "+request.form['lng']))
|
||||||
mongo.db.pins.insert_one(pin)
|
mongo.db.pins.insert_one(pin)
|
||||||
@ -70,7 +137,7 @@ def create_app(config=Config):
|
|||||||
def registrame(referral_code):
|
def registrame(referral_code):
|
||||||
inviter = mongo.db.users.find_one({"referral_code": referral_code})
|
inviter = mongo.db.users.find_one({"referral_code": referral_code})
|
||||||
if not inviter:
|
if not inviter:
|
||||||
message = "Ooops, parece que nadie te invitó aquí"
|
message = "Ooops, parece que nadie te invitó aquí... Puedes saber más sobre esta decisión aquí https://baches.qro.mx/quienes#preguntas"
|
||||||
return render_template('error.html', message = message), 406
|
return render_template('error.html', message = message), 406
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -84,13 +151,19 @@ def create_app(config=Config):
|
|||||||
"is_admin": False
|
"is_admin": False
|
||||||
}
|
}
|
||||||
new_user_id = mongo.db.users.insert_one(new_user_data).inserted_id
|
new_user_id = mongo.db.users.insert_one(new_user_data).inserted_id
|
||||||
|
str_id = str(new_user_id)
|
||||||
|
mongo.db.users.update_one(
|
||||||
|
{'_id': new_user_id},
|
||||||
|
{'$set': {'str_id': str_id}}
|
||||||
|
)
|
||||||
invite_link = url_for('registrame', referral_code=new_user_data['referral_code'], _external=True)
|
invite_link = url_for('registrame', referral_code=new_user_data['referral_code'], _external=True)
|
||||||
login_user(load_user(new_user_id))
|
login_user(load_user(new_user_id), remember=True)
|
||||||
|
flash('Usuario registrado exitosamente')
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
#return render_template('dashboard.html', invite_link=invite_link)
|
#return render_template('dashboard.html', invite_link=invite_link)
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return render_template('register.html', form=RegistrationForm())
|
return render_template('register.html', rform=RegistrationForm())
|
||||||
|
|
||||||
@app.route('/thelogin', methods=['GET', 'POST'])
|
@app.route('/thelogin', methods=['GET', 'POST'])
|
||||||
def thelogin():
|
def thelogin():
|
||||||
@ -101,11 +174,11 @@ def create_app(config=Config):
|
|||||||
user_data = mongo.db.users.find_one({"username": username})
|
user_data = mongo.db.users.find_one({"username": username})
|
||||||
if user_data and check_password_hash(user_data['pwd'], pwd):
|
if user_data and check_password_hash(user_data['pwd'], pwd):
|
||||||
user = User(user_data)
|
user = User(user_data)
|
||||||
login_user(user)
|
login_user(user, remember=True)
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
else:
|
else:
|
||||||
return render_template('login.html', messages = 'Ooops, no hay tal usuario', form=form2)
|
return render_template('login.html', messages = 'Ooops, no hay tal usuario', login_form=form2)
|
||||||
return render_template('login.html',form=form2)
|
return render_template('login.html',login_form=form2)
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
@ -151,29 +224,64 @@ def create_app(config=Config):
|
|||||||
else:
|
else:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route("/edit_pin/<pin_id>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_pin(pin_id):
|
||||||
|
actual_pin = mongo.db.pins.find_one({"_id": ObjectId(pin_id)})
|
||||||
|
if not actual_pin:
|
||||||
|
flash('Pin no encontrado')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
added_by = actual_pin.get("added_by")
|
||||||
|
|
||||||
|
if current_user.is_admin or current_user.id == added_by:
|
||||||
|
# Get form data
|
||||||
|
typeofpin = request.form.get('typeofpin')
|
||||||
|
description = request.form.get('description')
|
||||||
|
address = request.form.get('address')
|
||||||
|
|
||||||
|
# Update the pin
|
||||||
|
mongo.db.pins.update_one(
|
||||||
|
{"_id": ObjectId(pin_id)},
|
||||||
|
{"$set": {
|
||||||
|
"typeofpin": typeofpin,
|
||||||
|
"description": description,
|
||||||
|
"address": address,
|
||||||
|
"last_mod": datetime.now()
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
flash('Pin actualizado exitosamente')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
else:
|
||||||
|
flash('No tienes permiso para editar este pin')
|
||||||
|
return render_template('error.html', message='No tienes permiso para editar este pin'), 403
|
||||||
|
|
||||||
|
@app.route('/manifest.json')
|
||||||
|
def manifest():
|
||||||
|
return send_from_directory('static', 'manifest.json')
|
||||||
|
@app.route('/src')
|
||||||
|
def src():
|
||||||
|
return send_from_directory('static', 'src')
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
if request.method == 'GET':
|
#if request.method == 'GET':
|
||||||
if not current_user.is_authenticated:
|
#if current_user.is_authenticated == False:
|
||||||
return redirect(url_for('thelogin'))
|
# return redirect(url_for('thelogin'))
|
||||||
if not current_user.is_admin:
|
#else:
|
||||||
pins = list(mongo.db.pins.find({"added_by": current_user.id}))
|
old_qr = mongo.db.users.find_one({'_id': ObjectId(current_user.id)})
|
||||||
invite_code = str(uuid4())
|
print(old_qr.get('referral_code'))
|
||||||
qr_update = mongo.db.users.update_one({'_id': ObjectId(current_user.id)}, {'$set': {'referral_code': invite_code}})
|
invite_code = str(uuid4())
|
||||||
print(qr_update)
|
qr_update = mongo.db.users.update_one({'_id': ObjectId(current_user.id)}, {'$set': {'referral_code': invite_code}})
|
||||||
return render_template('dashboard.html', pins=pins, invite_code=invite_code)
|
print(invite_code)
|
||||||
if current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
users = list(mongo.db.users.find())
|
pins = list(mongo.db.pins.find({"added_by": current_user.id}).sort("time", -1))
|
||||||
pins = list(mongo.db.pins.find())
|
print(pins)
|
||||||
invite_code = str(uuid4())
|
return render_template('dashboard.html', pins=pins, invite_code=invite_code)
|
||||||
qr_update = mongo.db.users.update_one({'_id': ObjectId(current_user.id)}, {'$set': {'referral_code': invite_code}})
|
if current_user.is_admin:
|
||||||
print(qr_update)
|
users = list(mongo.db.users.find())
|
||||||
return render_template('dashboard.html', users=users, pins=pins, invite_code=invite_code)
|
pins = list(mongo.db.pins.find())
|
||||||
#if request.method == 'POST':
|
return render_template('dashboard.html', users=users, pins=pins, invite_code=invite_code)
|
||||||
# pass
|
|
||||||
|
|
||||||
@app.cli.command('add_user')
|
@app.cli.command('add_user')
|
||||||
def add_user():
|
def add_user():
|
||||||
@ -189,8 +297,103 @@ def create_app(config=Config):
|
|||||||
}
|
}
|
||||||
mongo.db.users.insert_one(admin_user)
|
mongo.db.users.insert_one(admin_user)
|
||||||
print(f"Admin {username} added! Referral code is {admin_user['referral_code']}")
|
print(f"Admin {username} added! Referral code is {admin_user['referral_code']}")
|
||||||
|
|
||||||
|
@app.route("/camera", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def camera():
|
||||||
|
if request.method == "POST":
|
||||||
|
# 1. Grab geolocation data from the form
|
||||||
|
latitude = request.form.get("latitude")
|
||||||
|
longitude = request.form.get("longitude")
|
||||||
|
|
||||||
|
# 2. Grab the photo file
|
||||||
|
photo = request.files.get("photo")
|
||||||
|
|
||||||
|
# 3. Save the file if it exists
|
||||||
|
if photo:
|
||||||
|
# Make sure your 'uploads' folder exists or handle dynamically
|
||||||
|
photo_path = os.path.join(app.config['UPLOAD_FOLDER'], photo.filename)
|
||||||
|
photo.save(photo_path)
|
||||||
|
|
||||||
|
# Create a new pin with the uploaded photo data
|
||||||
|
pin = {
|
||||||
|
'time': datetime.now(),
|
||||||
|
'photo': photo_path,
|
||||||
|
'lat': latitude,
|
||||||
|
'lng': longitude,
|
||||||
|
'typeofpin': request.form.get('typeofpin', 'bache'), # Default to bache if not specified
|
||||||
|
'added_by': current_user.id,
|
||||||
|
'description': request.form.get('description', 'Foto desde cámara'), # Default description
|
||||||
|
'reviewed': False, # Added reviewed field set to False
|
||||||
|
'address': str(geolocator.reverse(f"{latitude}, {longitude}")) if latitude and longitude else "Ubicación desconocida"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Insert the new pin into the database
|
||||||
|
mongo.db.pins.insert_one(pin)
|
||||||
|
flash('¡Gracias por tu aportación!')
|
||||||
|
# For demonstration, just redirect or return a success message
|
||||||
|
return f"Photo uploaded! Lat: {latitude}, Lng: {longitude}"
|
||||||
|
|
||||||
|
# If GET request, just render the camera page
|
||||||
|
return render_template("camera.html")
|
||||||
|
@app.route('/leaderboard')
|
||||||
|
def leaderboard():
|
||||||
|
pipeline = [
|
||||||
|
{"$group": {"_id": "$added_by", "count": {"$sum": 1}}},
|
||||||
|
{"$sort": {"count": -1}},
|
||||||
|
{"$limit": 10},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Convert the aggregation result to a list
|
||||||
|
leaders_by_id = list(mongo.db.pins.aggregate(pipeline))
|
||||||
|
|
||||||
|
# Create a list to store the final results
|
||||||
|
cleaned_leaders = []
|
||||||
|
|
||||||
|
# Process each leader
|
||||||
|
for leader in leaders_by_id:
|
||||||
|
user_id = leader["_id"]
|
||||||
|
count = leader["count"]
|
||||||
|
|
||||||
|
# Find the corresponding user
|
||||||
|
try:
|
||||||
|
user = mongo.db.users.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if user and "username" in user:
|
||||||
|
username = user["username"]
|
||||||
|
if len(username) >= 2:
|
||||||
|
cleaned_username = username[0] + "***" + username[-1]
|
||||||
|
else:
|
||||||
|
cleaned_username = username+"***"+random.choice(string.ascii_letters, k=1)
|
||||||
|
cleaned_leaders.append({"username": cleaned_username, "count": count})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing user_id {user_id}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print('THIS IS THE LEADERBOARD', cleaned_leaders)
|
||||||
|
# Get stats using aggregation pipeline
|
||||||
|
# Count total users
|
||||||
|
total_users_result = list(mongo.db.users.aggregate([
|
||||||
|
{"$count": "total"}
|
||||||
|
]))
|
||||||
|
total_users = total_users_result[0]["total"] if total_users_result else 0
|
||||||
|
|
||||||
|
# Count unique users who have added pins
|
||||||
|
active_users_result = list(mongo.db.pins.aggregate([
|
||||||
|
{"$group": {"_id": None, "active_users": {"$addToSet": "$added_by"}}},
|
||||||
|
{"$project": {"active_count": {"$size": "$active_users"}}}
|
||||||
|
]))
|
||||||
|
active_users = active_users_result[0]["active_count"] if active_users_result else 0
|
||||||
|
|
||||||
|
# Calculate percentage (handle case where total_users might be 0)
|
||||||
|
active_percentage = round((active_users / total_users) * 100, 1) if total_users > 0 else 0
|
||||||
|
|
||||||
|
return render_template('leaderboard.html', leaders=cleaned_leaders, percentage = active_percentage)
|
||||||
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app=create_app()
|
app=create_app()
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
|
from datetime import timedelta
|
||||||
class Config:
|
class Config:
|
||||||
MONGO_URI = 'mongodb://localhost:27017/mapDB'
|
MONGO_URI = 'mongodb://localhost:27017/mapDB'
|
||||||
UPLOAD_FOLDER = 'uploads'
|
UPLOAD_FOLDER = 'uploads'
|
||||||
SECRET_KEY = 'supersecretkey'
|
SECRET_KEY = 'supersecretkey'
|
||||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
LOGIN_URL = '/thelogin'
|
||||||
|
REMEMBER_COOKIE_DURATION = timedelta(days=7) # Adjust as needed
|
||||||
|
REMEMBER_COOKIE_HTTPONLY = True
|
||||||
|
REMEMBER_COOKIE_SECURE = True # Set to True if using HTTPS
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
58
forms.py
58
forms.py
@ -1,58 +0,0 @@
|
|||||||
from flask_login import UserMixin
|
|
||||||
from flask_pymongo import PyMongo, ObjectId
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from datetime import datetime
|
|
||||||
from wtforms import StringField, FileField, SubmitField, DateTimeField, SelectField, PasswordField
|
|
||||||
from wtforms.validators import DataRequired, Length
|
|
||||||
#Init Mongo
|
|
||||||
from db import mongo
|
|
||||||
#User object creation
|
|
||||||
|
|
||||||
class User(UserMixin):
|
|
||||||
def __init__(self,user_data):
|
|
||||||
self.id = str(user_data['_id'])
|
|
||||||
self.username = user_data["username"]
|
|
||||||
self.referral_code = user_data["referral_code"]
|
|
||||||
self.invited_by = user_data.get('invited_by')
|
|
||||||
self.is_admin = user_data.get('is_admin', False)
|
|
||||||
seelf.pwd = user_data.get('pwd')
|
|
||||||
|
|
||||||
#Getter method
|
|
||||||
@staticmethod
|
|
||||||
def get(user_id):
|
|
||||||
user_data = mongo.db.users.find_one({"_id": ObjectId(user_id)})
|
|
||||||
if user_data:
|
|
||||||
return User(user_data)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
#Pin creation form
|
|
||||||
class PinForm(FlaskForm):
|
|
||||||
description = StringField('¿Qué estamos viendo?', validators=[DataRequired()])
|
|
||||||
photo = FileField('Evidencia fotogénica', validators=[DataRequired()])
|
|
||||||
timedate = DateTimeField(default=datetime.now())
|
|
||||||
typeofpin = SelectField('Tipo de cosa', choices=['bache', 'coladera', 'obra sin terminar', 'escombro', 'robo-asalto', 'biciestacionamiento', 'mala iluminación', 'bici blanca', 'zapato blanco'])
|
|
||||||
submit = SubmitField('Agregar')
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
|
||||||
username = StringField('Usuario', validators=[DataRequired()])
|
|
||||||
pwd = PasswordField('Tu clave', validators= [DataRequired()])
|
|
||||||
submit = SubmitField('Entrar')
|
|
||||||
|
|
||||||
#decorator to simplify mongo unique validation calls
|
|
||||||
def Unique(model, field, message=None):
|
|
||||||
def _unique(form, field_data):
|
|
||||||
if mongo.db[model.__name__.lower()].find_one({field.name: field_data.data}):
|
|
||||||
raise ValidationError(message or f"{field.name} must be unique.")
|
|
||||||
return _unique
|
|
||||||
|
|
||||||
class RegistrationForm(FlaskForm):
|
|
||||||
username = StringField('Nombre de usuarix', validators=[DataRequired(), Unique('users', StringField('username', message="Este usuario ya existe"))])
|
|
||||||
pwd = PasswordField('Clave', validators=[DataRequired(), Length(min=10), Unique('users', StringField('pwd', message="Esta clave no es muy buena, escoge otra"))])
|
|
||||||
referral = StringField('ID de quien te invito', [DataRequired()])
|
|
||||||
submit = SubmitField('Registrar')
|
|
||||||
|
|
||||||
|
|
||||||
#restrict allowed file types~
|
|
||||||
def allowed_file(filename):
|
|
||||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
|
||||||
@ -7,8 +7,6 @@ Flask==3.0.3
|
|||||||
Flask-Login==0.6.3
|
Flask-Login==0.6.3
|
||||||
Flask-PyMongo==2.3.0
|
Flask-PyMongo==2.3.0
|
||||||
Flask-WTF==1.2.1
|
Flask-WTF==1.2.1
|
||||||
geographiclib==2.0
|
|
||||||
geopy==2.4.1
|
|
||||||
idna==3.8
|
idna==3.8
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.3
|
Jinja2==3.1.3
|
||||||
|
|||||||
1405
static/MarkerClusterGroup.js
Normal file
1405
static/MarkerClusterGroup.js
Normal file
File diff suppressed because it is too large
Load Diff
14
static/images/bachemapa.svg
Normal file
14
static/images/bachemapa.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Map pin -->
|
||||||
|
<circle cx="100" cy="40" r="12" fill="#4A90E2"/>
|
||||||
|
<path d="M50,55 Q60,45 70,55 Q80,65 90,55" stroke="#888" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M100,45 L105,60 L95,60 Z" fill="#fff"/>
|
||||||
|
|
||||||
|
<!-- Pothole shape -->
|
||||||
|
<ellipse cx="100" cy="90" rx="40" ry="10" fill="#bbb" stroke="#777" stroke-width="2" />
|
||||||
|
|
||||||
|
<!-- Typography -->
|
||||||
|
<text x="100" y="130" font-family="Montserrat, sans-serif" font-size="16" fill="#333" text-anchor="middle">
|
||||||
|
Bachemapa
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 580 B |
BIN
static/images/bachemapalogo.png
Normal file
BIN
static/images/bachemapalogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
BIN
static/images/bg-trees.jpg
Normal file
BIN
static/images/bg-trees.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
BIN
static/images/contract.png
Normal file
BIN
static/images/contract.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
static/images/contract2.png
Normal file
BIN
static/images/contract2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
static/images/favico.ico
Normal file
BIN
static/images/favico.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
File diff suppressed because one or more lines are too long
21
static/manifest.json
Normal file
21
static/manifest.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "El glorioso y nunca bien ponderado Bachemapa",
|
||||||
|
"short_name": "Bachemapa",
|
||||||
|
"start_url": "/camera",
|
||||||
|
"scope": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/images/contract.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/images/contract2.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffd31d",
|
||||||
|
"background_color": "#333",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
42
static/service-worker.js
Normal file
42
static/service-worker.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const CACHE_NAME = 'bachemapa-pwa-v2';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/quienes',
|
||||||
|
'/dashboard',
|
||||||
|
'/leaderboard',
|
||||||
|
'/thelogin',
|
||||||
|
'/static/css/main.css',
|
||||||
|
'/static/js/main.js',
|
||||||
|
'/static/js/leaflet.js',
|
||||||
|
'/static/css/styles.css',
|
||||||
|
'/static/images/marker-icon.png',
|
||||||
|
'/static/images/marker-shadow.png',
|
||||||
|
'/static/images/leaflet.css',
|
||||||
|
'/static/images/contract.png',
|
||||||
|
'/static/images/contract2.png',
|
||||||
|
'/static/manifest.json',
|
||||||
|
'/manifest.json',
|
||||||
|
'/static/service-worker.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(cache => {
|
||||||
|
console.log('Opened cache');
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => {
|
||||||
|
// Return cached asset if available, otherwise fetch from network
|
||||||
|
return response || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -1,166 +1,507 @@
|
|||||||
|
:root {
|
||||||
|
/* Color palette */
|
||||||
|
--primary-color: rgba(202, 216, 3, 0.9);
|
||||||
|
--secondary-color: green;
|
||||||
|
--accent-color: rgb(255, 136, 0);
|
||||||
|
--background-color: rgb(205, 243, 148);
|
||||||
|
--text-color: #333;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--spacing-sm: 0.5rem;
|
||||||
|
--spacing-md: 1rem;
|
||||||
|
--spacing-lg: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
body, html {
|
body, html {
|
||||||
font-family: Arial, sans-serif;
|
font-family: 'Poppins', 'Montserrat', 'Segoe UI', Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
/* height: 100%; */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
color: var(--text-color);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
|
|
||||||
section#pinner-modal form {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: rgba(202, 216, 3, 0.7);
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
form p {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0 0;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
h1::after {
|
||||||
margin:0;
|
content: '';
|
||||||
padding:0;
|
display: block;
|
||||||
|
width: 50px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
margin: 8px auto;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
#map {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 1; /* Ensure it is behind other elements */
|
||||||
}
|
}
|
||||||
|
|
||||||
div.leaflet-popup-content {
|
main {
|
||||||
|
margin: 0;
|
||||||
}
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
section#pinner-modal {
|
z-index: 5; /* Ensure it is above the map */
|
||||||
position: absolute;
|
|
||||||
top: 7vh;
|
|
||||||
left: 0rem;
|
|
||||||
color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
transition: bottom 0.5s ease;
|
|
||||||
box-shadow: 0px -4px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
z-index: 999;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 1;
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden; /* Prevent scrollbars */
|
||||||
}
|
}
|
||||||
|
|
||||||
section.pinner-modal.active {
|
footer {
|
||||||
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(202, 216, 3, 0.7);
|
max-height: 10vh;
|
||||||
|
text-align: left;
|
||||||
|
align-content: left;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background-color:rgba(202, 216, 3, 0.7);
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid greenyellow;
|
||||||
|
padding-left: 15%;
|
||||||
|
padding-right:15%;
|
||||||
|
z-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.pinner-modal form {
|
table#mapistas thead tr th {
|
||||||
background-color: rgba(202, 216, 3, 0.7);
|
background-color: rgba(202,216,3,0.7);
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table#mapistas tbody tr td {
|
||||||
|
background-color: rgba(202,216,3,0.7);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
position: fixed;
|
||||||
|
top: 12%; /* Space for nav */
|
||||||
|
bottom: 10%; /* Space for footer */
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
max-height: calc(100vh - 90px); /* Adjust based on nav and footer heights */
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 80%;
|
||||||
|
display: block;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
nav {
|
nav {
|
||||||
position:absolute;
|
position: absolute;
|
||||||
top:0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: rgba(202, 216, 3, 0.7);
|
padding-left: 10vw;
|
||||||
padding: 5px;
|
/* padding-right:10vw; */
|
||||||
|
background-color: rgba(189, 216, 3, 0.24);
|
||||||
|
/* padding: var(--spacing-sm); */
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
overflow: hidden;
|
box-shadow: 0 2px 10px var(--shadow-color);
|
||||||
padding-right: 0.1rem;
|
backdrop-filter: blur(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul {
|
nav ul {
|
||||||
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: 0 var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul li {
|
nav ul li {
|
||||||
padding-top: 0;
|
padding: 0;
|
||||||
padding-bottom: 0;
|
flex: 1 1 auto;
|
||||||
flex:1 1 auto;
|
list-style: none;
|
||||||
|
}
|
||||||
|
nav li [role=button],
|
||||||
|
nav li [type=button],
|
||||||
|
nav li button {
|
||||||
|
background-color: rgba(36, 82, 1, 0.9);
|
||||||
|
backdrop-filter: blur(2px) contrast(1.2);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
border-width: .2rem;
|
||||||
|
padding: 0.1rem 1rem;
|
||||||
|
/* border-radius: 4px; */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
background-color: rgba(255, 136, 0, 0.8);
|
||||||
|
}
|
||||||
|
.mobile-menu ul li {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
border-radius: 100rem;
|
||||||
|
width: auto;
|
||||||
|
/* backdrop-filter: hue-rotate(180deg); */
|
||||||
|
/* background-color: rgba(255, 255, 255, 0.7); */
|
||||||
|
color: rgba(172, 255, 47, 0.9);
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-marker-icon {
|
.mobile-menu ul li a:hover {
|
||||||
background-color: transparent !important;
|
backdrop-filter: blur(2px) saturate(150%) brightness(1.5) hue-rotate(180deg);
|
||||||
border: rgba(0, 0, 0, 0);
|
animation: pulse 1s normal ease-in-out;
|
||||||
|
/* box-shadow: 0 0 -2px rgba(192, 157, 117, 0.8); */
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
/* transform: scale(1.2); */
|
||||||
|
color: black;
|
||||||
|
color: rgba(255, 136, 0, 1);
|
||||||
|
z-index: 1800;
|
||||||
|
/* padding: 0; */
|
||||||
|
}
|
||||||
|
/* .mobile-menu ul li a i:hover {
|
||||||
|
|
||||||
|
} */
|
||||||
|
/* .mobile-menu ul li a i {
|
||||||
|
|
||||||
|
} */
|
||||||
|
|
||||||
|
.mobile-menu ul li a {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-radius: 2rem;
|
||||||
|
backdrop-filter: invert(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu ul li a i{
|
||||||
|
margin-left:0px;
|
||||||
|
padding: .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-menu {
|
||||||
|
display: block;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-cluster {
|
.desktop-menu li a {
|
||||||
background-color: greenyellow !important;
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
.fa, .far, .fas {
|
|
||||||
color:black;
|
|
||||||
text-shadow: -1px 0 rgb(255, 136, 0), 0 1px rgb(255, 136, 0), 1px 0 rgb(255, 136, 0), 0 -1px rgb(255, 136, 0);
|
|
||||||
margin-left: -10px;
|
|
||||||
margin-top: -10px;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
filter:drop-shadow(0 0 0.2rem orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
path-derping {
|
|
||||||
fill:orangered;
|
|
||||||
stroke: orangered;
|
|
||||||
stroke-width:1px;
|
|
||||||
stroke-dasharray: 2,2;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flashes {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
z-index: 1001;
|
justify-content: space-between;
|
||||||
border-radius: 20px;
|
align-items: center;
|
||||||
background-color: rgba(202, 216, 3, 0.7);
|
|
||||||
top: 8%;
|
|
||||||
padding: 1rem;
|
|
||||||
border: black;
|
|
||||||
border-width: 1px;
|
|
||||||
-webkit-animation: cssAnimation 7s forwards;
|
|
||||||
animation: cssAnimation 7s forwards;
|
|
||||||
}
|
}
|
||||||
@keyframes cssAnimation {
|
|
||||||
0% {opacity: 1;}
|
.desktop-menu li a:hover {
|
||||||
90% {opacity: 1;}
|
background-color: rgb(45, 60, 35);
|
||||||
100% {opacity: 0;}
|
animation: pulse 1.5s infinite ease-in-out;
|
||||||
|
box-shadow: 0 0 -3px rgba(255, 167, 14, 0.7);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
@-webkit-keyframes cssAnimation {
|
|
||||||
0% {opacity: 1;}
|
/* Buttons */
|
||||||
90% {opacity: 1;}
|
button {
|
||||||
100% {opacity: 0;}
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 150%;
|
||||||
|
width: 150%;
|
||||||
|
top: -25%;
|
||||||
|
left: -25%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.3) 100%, transparent 10.01%);
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: rgba(255, 136, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover::after {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pinner-top {
|
#pinner-top {
|
||||||
background-color: green;
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
input#submit {
|
|
||||||
background-color: green;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button a {
|
button a {
|
||||||
color: black;
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
/* Form elements */
|
||||||
display: flex;
|
input, select, textarea {
|
||||||
align-items: center;
|
border: 1px solid #ddd;
|
||||||
justify-content: center;
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
transition: border 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 136, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
input#submit {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#submit:hover {
|
||||||
|
background-color: #008800;
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#lng, #lat {
|
#lng, #lat {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
|
color: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
section#pinner-modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 10vh;
|
||||||
|
left:10%;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.5s forwards;
|
||||||
|
box-shadow: 0 4px 20px var(--shadow-color);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: rgba(189, 216, 3, 0.24);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#pinner-modal form {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#pinner-modal::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#pinner-modal::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map elements */
|
||||||
|
.leaflet-marker-icon {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
.eaflet-popup-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.fa, .far, .fas {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
/* filter: drop-shadow(0 0 0.2rem var(--accent-color)); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
color: tomato !important;
|
||||||
|
filter: drop-shadow 0 0 0.2rem var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
/* right: 20%; */
|
||||||
|
width: 15vw;
|
||||||
|
height: 15vw;
|
||||||
|
/* transform: translateX(-50%); */
|
||||||
|
background-color: rgba(60, 245, 9, 0.8);
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.5);
|
||||||
|
/* font-size: 4rem; */
|
||||||
|
font-weight: bolder;
|
||||||
|
z-index: 100;
|
||||||
|
color: white;
|
||||||
|
animation: pulsePing 1s ease-in-out 0s 1 normal none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button .fas.fa-camera {
|
||||||
|
font-size: 3rem; /* Increased size specifically for camera icon */
|
||||||
|
}
|
||||||
|
.marker-cluster {
|
||||||
|
background-color: rgb(249, 170, 61) !important;
|
||||||
|
border-radius: 100px;
|
||||||
|
box-shadow: 0 0 10px rgba(85, 38, 5, 0.7);
|
||||||
|
/* Animation removed as it was causing visibility issues */
|
||||||
|
}
|
||||||
|
|
||||||
|
#dash-container {
|
||||||
|
max-width: 75vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
.flashes {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 1001;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: rgba(189, 216, 3, 0.6);
|
||||||
|
top: 12vh;
|
||||||
|
left: 10vw;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 4px 15px var(--shadow-color);
|
||||||
|
animation: fadeOut 15s forwards;
|
||||||
|
}
|
||||||
|
ul.flashes li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*/html/body/main/div[1]/div[1]/div[2]/svg/g/path */
|
||||||
|
|
||||||
|
path {
|
||||||
|
cursor: crosshair !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% { opacity: 1; transform: translateY(0); }
|
||||||
|
90% { opacity: 1; transform: translateY(0); }
|
||||||
|
100% { opacity: 0; transform: translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% { opacity: 0; transform: translateY(0); }
|
||||||
|
90% { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { box-shadow: 0 0 10px rgba(172, 255, 47, 0.7); }
|
||||||
|
50% { box-shadow: 0 0 15px rgba(172, 255, 47, 0.9); }
|
||||||
|
100% { box-shadow: 0 0 10px rgba(172, 255, 47, 0.7); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulsePing {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Media queries for responsiveness */
|
||||||
|
@media (max-width: 811px) {
|
||||||
|
section#pinner-modal {
|
||||||
|
width: 90%;
|
||||||
|
left: 5%;
|
||||||
|
right: 5%;
|
||||||
|
}
|
||||||
|
img#logo {
|
||||||
|
max-height: 5vh;
|
||||||
|
}
|
||||||
|
nav {
|
||||||
|
height: 10vh;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#dash-container {
|
||||||
|
max-width: 95vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 812px) {
|
||||||
|
nav {
|
||||||
|
height: 10vh;
|
||||||
|
}
|
||||||
|
nav ul {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
nav ul li {
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
}
|
||||||
|
nav ul li a {
|
||||||
|
padding: 0.5rem 1
|
||||||
|
}
|
||||||
|
.control-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
379
static/stylesbs.css
Normal file
379
static/stylesbs.css
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
:root {
|
||||||
|
/* Color palette */
|
||||||
|
--primary-color: rgba(202, 216, 3, 0.9);
|
||||||
|
--secondary-color: green;
|
||||||
|
--accent-color: rgb(255, 136, 0);
|
||||||
|
--background-color: rgb(205, 243, 148);
|
||||||
|
--text-color: #333;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--spacing-sm: 0.5rem;
|
||||||
|
--spacing-md: 1rem;
|
||||||
|
--spacing-lg: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
body, html {
|
||||||
|
font-family: 'Poppins', 'Montserrat', 'Segoe UI', Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 50px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
margin: 8px auto;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
#map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
position: static !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 10vh;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom button styling */
|
||||||
|
.custom-btn {
|
||||||
|
background-color: rgba(255, 136, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-btn:hover {
|
||||||
|
background-color: rgb(74, 133, 34);
|
||||||
|
animation: pulse 1.5s infinite ease-in-out;
|
||||||
|
box-shadow: 0 0 10px rgba(172, 255, 47, 0.7);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-link {
|
||||||
|
background-color: rgba(255, 136, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-link:hover {
|
||||||
|
background-color: rgb(74, 133, 34);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
max-height: 10vh;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background-color: rgba(202, 216, 3, 0.7);
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid greenyellow;
|
||||||
|
padding-left: 15%;
|
||||||
|
padding-right: 15%;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
|
.custom-table thead th {
|
||||||
|
background-color: rgba(202, 216, 3, 0.7);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table tbody td {
|
||||||
|
background-color: rgba(202, 216, 3, 0.7);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
position: fixed;
|
||||||
|
top: 12%;
|
||||||
|
bottom: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
max-height: calc(100vh - 90px);
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 80%;
|
||||||
|
display: block;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.custom-navbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-left: 10vw;
|
||||||
|
background-color: rgba(189, 216, 3, 0.24);
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px var(--shadow-color);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar .navbar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: 0 var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar .nav-item {
|
||||||
|
padding: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
.custom-form-control {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
transition: border 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 136, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
background-color: #008800;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinate-display {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background-color: grey;
|
||||||
|
color: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.custom-modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 10vh;
|
||||||
|
left: 10%;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
text-align: left;
|
||||||
|
animation: fadeIn 0.5s forwards;
|
||||||
|
box-shadow: 0 4px 20px var(--shadow-color);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: rgba(189, 216, 3, 0.24);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal form {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map elements */
|
||||||
|
.leaflet-marker-icon {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 15px 30px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 50px;
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
z-index: 100;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster {
|
||||||
|
background-color: rgb(249, 170, 61) !important;
|
||||||
|
border-radius: 100px;
|
||||||
|
box-shadow: 0 0 10px rgba(85, 38, 5, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa, .far, .fas {
|
||||||
|
color: black;
|
||||||
|
text-shadow: -1px 0 var(--accent-color), 0 1px var(--accent-color),
|
||||||
|
1px 0 var(--accent-color), 0 -1px var(--accent-color);
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-top: -10px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
filter: drop-shadow(0 0 0.2rem var(--accent-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
.alert-custom {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 1001;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: rgba(189, 216, 3, 0.6);
|
||||||
|
top: 12vh;
|
||||||
|
left: 10vw;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 4px 15px var(--shadow-color);
|
||||||
|
animation: fadeOut 15s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-custom li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel styles */
|
||||||
|
.carousel-control {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-control:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-items {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-items::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% { opacity: 1; transform: translateY(0); }
|
||||||
|
90% { opacity: 1; transform: translateY(0); }
|
||||||
|
100% { opacity: 0; transform: translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% { opacity: 0; transform: translateY(0); }
|
||||||
|
90% { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { box-shadow: 0 0 10px rgba(172, 255, 47, 0.7); }
|
||||||
|
50% { box-shadow: 0 0 15px rgba(172, 255, 47, 0.9); }
|
||||||
|
100% { box-shadow: 0 0 10px rgba(172, 255, 47, 0.7); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media queries for responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.custom-modal {
|
||||||
|
width: 90%;
|
||||||
|
left: 5%;
|
||||||
|
right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img#logo {
|
||||||
|
max-height: 5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar {
|
||||||
|
height: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar .navbar-nav {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<form method="post" enctype="multipart/form-data" >
|
<form method="post" enctype="multipart/form-data" >
|
||||||
<progress id="progress" hidden="true"></progress>
|
<progress id="progress" hidden="true"></progress>
|
||||||
|
{% if form %}
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<!--<p>
|
<!--<p>
|
||||||
{{ form.description.label }}<br>
|
{{ form.description.label }}<br>
|
||||||
@ -24,9 +24,18 @@
|
|||||||
<td style="background-color:transparent"><button style="background-color: red; color:white" id="cancel-add" type="button">Cancelar</button></td>
|
<td style="background-color:transparent"><button style="background-color: red; color:white" id="cancel-add" type="button">Cancelar</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<script>
|
||||||
|
submitButton = document.getElementById('submit');
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.addEventListener('click', function() {
|
||||||
|
// Disable the button after submission
|
||||||
|
submitButton.disabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
document.getElementById('submit').onclick=function(e){
|
document.getElementById('submit').onclick=function(e){
|
||||||
document.getElementById('progress').hidden=''
|
document.getElementById('progress').hidden='false'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,29 +1,67 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-theme="light" lang="es" style="background-color: rgb(205, 243, 148); color: black;">
|
<html data-theme="light" lang="es" style="background-color: rgb(205, 243, 148); color: black;">
|
||||||
<head>
|
<head>
|
||||||
|
<!-- BASE !-->
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Bachemapa - Mapa interactivo de baches y otras cosas</title>
|
<title>Bachemapa - Mapa interactivo de baches y otras cosas</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='pico.amber.css') }}">
|
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='pico.amber.css') }}"> -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||||
crossorigin=""/>
|
crossorigin=""/>
|
||||||
|
<!-- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.src.js"
|
||||||
|
integrity="sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
|
||||||
|
crossorigin=""></script> -->
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
crossorigin=""></script>
|
crossorigin=""></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='images/favico.ico') }}" type="image/x-icon">
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='images/favico.ico') }}" type="image/x-icon">
|
||||||
|
<!-- <script src="https://app.unpkg.com/leaflet.markercluster@1.4.1/files/dist/leaflet.markercluster.js" type="text/javascript"></script> -->
|
||||||
|
<script type="application/importmap" src="https://app.unpkg.com/leaflet.markercluster@1.4.1/files/dist/leaflet.markercluster-src.js.map"></script>
|
||||||
|
<link href="https://app.unpkg.com/leaflet.markercluster@1.4.1/files/dist/MarkerCluster.css">
|
||||||
|
<link href="https://app.unpkg.com/leaflet.markercluster@1.4.1/files/dist/MarkerCluster.Default.css">
|
||||||
|
<script src="{{ url_for('static', filename='leaflet.markercluster.js')}}"></script>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
|
||||||
|
<!-- <script src="{{ url_for('static', filename='src/MarkerClusterGroup.js')}}"></script> -->
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='leaflet.markercluster.js')}}"></script>
|
<!-- <script type="importmap" src="{{ url_for('static', filename='leaflet.markercluster.js.map')}}"></script>
|
||||||
<script type="importmap" src="{{ url_for('static', filename='leaflet.markercluster.js.map')}}"></script>
|
|
||||||
<link href="{{ url_for('static', filename='MarkerCluster.css')}}">
|
<link href="{{ url_for('static', filename='MarkerCluster.css')}}">
|
||||||
<link href="{{ url_for('static', filename='MarkerCluster.Default.css')}}">
|
<link href="{{ url_for('static', filename='MarkerCluster.Default.css')}}"> -->
|
||||||
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
|
|
||||||
<meta name="description" content="Bachemapa es una plataforma dedicada a documentar y reportar problemas con el desarrollo urbano en Querétaro, México. Encuentra y aporta información actualizada sobre baches, deficiencias en infraestructura y más.">
|
<!-- Primary Meta Tags -->
|
||||||
|
<meta name="description" content="Bachemapa: plataforma colaborativa para documentar y visualizar el estado de la infraestructura urbana en Querétaro, México. Información actualizada sobre mantenimiento vial e infraestructura pública.">
|
||||||
|
<meta name="keywords" content="Querétaro, infraestructura urbana, mantenimiento vial, desarrollo urbano, mapeo colaborativo, documentación urbana, participación ciudadana, urbanización, planificación urbana, vialidades, reporte de baches">
|
||||||
|
|
||||||
<!-- Palabras clave para SEO -->
|
<!-- Schema.org markup for Google -->
|
||||||
<meta name="keywords" content="Querétaro, problemas urbanos, baches, infraestructura, desarrollo urbano, reporte de baches, comunidad, ciudadanía, urbanización, planificación, deficiencias urbanas">
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebApplication",
|
||||||
|
"name": "Bachemapa",
|
||||||
|
"description": "Plataforma para documentar y visualizar el estado de la infraestructura urbana en Querétaro",
|
||||||
|
"url": "https://baches.qro.mx",
|
||||||
|
"applicationCategory": "MapApplication",
|
||||||
|
"operatingSystem": "All",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "MXN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Robots meta tag -->
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
|
||||||
|
<!-- Language and geo tags -->
|
||||||
|
<meta name="language" content="es">
|
||||||
|
<meta name="geo.region" content="MX-QUE">
|
||||||
|
<meta name="geo.placename" content="Querétaro">
|
||||||
|
|
||||||
<!-- Autor de la página -->
|
<!-- Autor de la página -->
|
||||||
<meta name="author" content="Kernel Panic Room">
|
<meta name="author" content="Kernel Panic Room">
|
||||||
@ -41,25 +79,13 @@
|
|||||||
|
|
||||||
<!-- Otros meta tags -->
|
<!-- Otros meta tags -->
|
||||||
<link rel="canonical" href="https://baches.qro.mx">
|
<link rel="canonical" href="https://baches.qro.mx">
|
||||||
|
{% block head %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Versión pública 0.1 -->
|
<!-- Versión pública 0.1 -->
|
||||||
</head>
|
</head>
|
||||||
<body class="" style="color: black;">
|
<body class="" style="color: black;">
|
||||||
<nav style="height: auto;">
|
{% include 'nav.html' %}
|
||||||
<ul>
|
|
||||||
<a href="/"><li><h3 style="margin-bottom: 0;">Bachemapa</h3></li></a>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
|
|
||||||
<li><button id="pinner-top">Agregar</button></li>
|
|
||||||
<li><button><a href="/quienes">Somos</a></button></li>
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
<li><button><a href="/dashboard">Perfil</a></button></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages() %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
|
||||||
@ -70,21 +96,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<main class="" style="padding:0px;height: 100vh; width:100vw; z-index: 5; position: absolute;">
|
<main class="container-fluid" >
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<section id="pinner-modal" style="" hidden="true">
|
<section id="pinner-modal" style="" hidden="true">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{% include 'add_pin.html' %}
|
{% include 'add_pin.html' %}
|
||||||
{% else %}
|
</section>
|
||||||
<div class="flashes">
|
{% else %}
|
||||||
<h3>Seguramente quieres hacer <a href="{{ url_for('thelogin') }}">login</a></h3>
|
<div class="flashes">
|
||||||
</div>
|
<h3>Seguramente quieres <a href="{{ url_for('thelogin') }}">iniciar sesión</a></h3>
|
||||||
{% endif %}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
{% endif %}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
@ -101,9 +129,13 @@
|
|||||||
modal_stat.hidden = false;
|
modal_stat.hidden = false;
|
||||||
} else {modal_stat.hidden = true;}
|
} else {modal_stat.hidden = true;}
|
||||||
}
|
}
|
||||||
pinner_button_top.addEventListener('click', toggleSlide);
|
if (pinner_button_top) {
|
||||||
|
pinner_button_top.addEventListener('click', toggleSlide);
|
||||||
|
}
|
||||||
const cancel_add = document.getElementById("cancel-add");
|
const cancel_add = document.getElementById("cancel-add");
|
||||||
cancel_add.addEventListener('click', toggleSlide);
|
if (cancel_add) {
|
||||||
|
cancel_add.addEventListener('click', toggleSlide);
|
||||||
|
}
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
@ -118,5 +150,25 @@ window.addEventListener('load', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pinnerDesktop = document.getElementById("pinner-top-desktop");
|
||||||
|
if (pinnerDesktop) {
|
||||||
|
pinnerDesktop.addEventListener('click', toggleSlide);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/static/service-worker.js')
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('Service Worker registered with scope:', registration.scope);
|
||||||
|
}, function(err) {
|
||||||
|
console.log('Service Worker registration failed:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
258
templates/camera.html
Normal file
258
templates/camera.html
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block head %}
|
||||||
|
<title>Take a Photo with Geolocation</title>
|
||||||
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#camera-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
#canvas {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
#file-fallback {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 100px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
#location-status {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="camera-container">
|
||||||
|
<video id="video" autoplay playsinline></video>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="location-status">Esperando ubicación...</div>
|
||||||
|
|
||||||
|
<div style="position: absolute; bottom: 0; width: 100%; display: flex; flex-direction: column; padding: 10px; box-sizing: border-box;">
|
||||||
|
<button id="capture-btn" class="control-button" style="margin: 10px; right:20%"><i class="fa fas fa-camera"></i></button>
|
||||||
|
<div style="display: flex; justify-content: space-between; width: 100%; box-sizing: border-box;">
|
||||||
|
<button id="retake-btn" class="control-button" style="display: none; margin: 10px 5px 10px 10px; flex: 1; left:20%; background-color: rgba(231, 9, 9, 0.671);color:white; left:20%"><i class="fas fa-redo"></i></button>
|
||||||
|
<button id="send-btn" class="control-button" style="display: none; margin: 10px 10px 10px 5px; flex: 1; background-color: rgba(49, 182, 28, 0.6);color:white; right: 20%;" disabled><i class="fas fa-paper-plane"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="camera-form" action="{{ url_for('camera') }}" method="POST" enctype="multipart/form-data" style="display:none;">
|
||||||
|
<input type="hidden" name="latitude" id="latitude" value="">
|
||||||
|
<input type="hidden" name="longitude" id="longitude" value="">
|
||||||
|
<input type="hidden" name="has_image" id="has_image" value="false">
|
||||||
|
|
||||||
|
<div id="file-fallback">
|
||||||
|
<label for="photo">Toma o selecciona una foto:</label>
|
||||||
|
<input type="file" name="photo" id="photo" accept="image/*" capture="environment" required>
|
||||||
|
<button type="submit">Subir</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Get DOM elements
|
||||||
|
const video = document.getElementById('video');
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const captureBtn = document.getElementById('capture-btn');
|
||||||
|
const retakeBtn = document.getElementById('retake-btn');
|
||||||
|
const sendBtn = document.getElementById('send-btn');
|
||||||
|
const form = document.getElementById('camera-form');
|
||||||
|
const fallbackInput = document.getElementById('file-fallback');
|
||||||
|
const hasImageInput = document.getElementById('has_image');
|
||||||
|
const locationStatus = document.getElementById('location-status');
|
||||||
|
|
||||||
|
// Location tracking variables
|
||||||
|
let hasLocation = false;
|
||||||
|
let watchId = null;
|
||||||
|
|
||||||
|
// Set up camera stream
|
||||||
|
async function setupCamera() {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
facingMode: 'environment',
|
||||||
|
width: { ideal: window.innerWidth },
|
||||||
|
height: { ideal: window.innerHeight }
|
||||||
|
},
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
video.srcObject = stream;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error accessing camera:', err);
|
||||||
|
fallbackInput.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize camera if supported
|
||||||
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||||
|
setupCamera();
|
||||||
|
} else {
|
||||||
|
console.error('getUserMedia not supported');
|
||||||
|
fallbackInput.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can enable the send button
|
||||||
|
function updateSendButtonState() {
|
||||||
|
if (hasImageInput.value === 'true' && hasLocation) {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.style.opacity = '1';
|
||||||
|
} else {
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.style.opacity = '0.5';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture photo from video feed
|
||||||
|
captureBtn.addEventListener('click', function() {
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Stop camera stream
|
||||||
|
video.srcObject.getTracks().forEach(track => track.stop());
|
||||||
|
video.style.display = 'none';
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
|
||||||
|
// Update controls
|
||||||
|
captureBtn.style.display = 'none';
|
||||||
|
retakeBtn.style.display = 'inline-block';
|
||||||
|
sendBtn.style.display = 'inline-block';
|
||||||
|
|
||||||
|
// Mark that we have an image
|
||||||
|
hasImageInput.value = 'true';
|
||||||
|
|
||||||
|
// Check if we can enable send button
|
||||||
|
updateSendButtonState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retake photo
|
||||||
|
retakeBtn.addEventListener('click', function() {
|
||||||
|
canvas.style.display = 'none';
|
||||||
|
video.style.display = 'block';
|
||||||
|
captureBtn.style.display = 'flex';
|
||||||
|
retakeBtn.style.display = 'none';
|
||||||
|
sendBtn.style.display = 'none';
|
||||||
|
hasImageInput.value = 'false';
|
||||||
|
setupCamera();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send photo
|
||||||
|
sendBtn.addEventListener('click', function() {
|
||||||
|
if (hasImageInput.value === 'true' && hasLocation) {
|
||||||
|
// Convert canvas to blob and append to FormData
|
||||||
|
canvas.toBlob(function(blob) {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.delete('photo'); // Remove any file input value
|
||||||
|
// Generate a filename with timestamp and random element
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const randomId = Math.floor(Math.random() * 10000);
|
||||||
|
const filename = `photo-${timestamp}-${randomId}.jpg`;
|
||||||
|
|
||||||
|
// Add the photo blob with the dynamic filename
|
||||||
|
formData.append('photo', blob, filename);
|
||||||
|
|
||||||
|
// Submit form data via fetch
|
||||||
|
fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.redirected) {
|
||||||
|
window.location.href = response.url;
|
||||||
|
} else if(response.ok) {
|
||||||
|
console.log('Success:', response);
|
||||||
|
window.location.href = '{{ url_for('dashboard') }}';
|
||||||
|
}
|
||||||
|
}).catch(error => console.error('Error:', error));
|
||||||
|
}, 'image/jpeg', 0.9);
|
||||||
|
} else if (!hasLocation) {
|
||||||
|
alert('Esperando ubicación GPS. Por favor espera.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request Geolocation Permission and Set Form Fields
|
||||||
|
if ('geolocation' in navigator) {
|
||||||
|
locationStatus.textContent = 'Obteniendo ubicación...';
|
||||||
|
locationStatus.style.backgroundColor = 'rgba(255, 165, 0, 0.7)'; // Orange
|
||||||
|
|
||||||
|
// Start watching position for more accurate results
|
||||||
|
watchId = navigator.geolocation.watchPosition(
|
||||||
|
(position) => {
|
||||||
|
document.getElementById('latitude').value = position.coords.latitude;
|
||||||
|
document.getElementById('longitude').value = position.coords.longitude;
|
||||||
|
hasLocation = true;
|
||||||
|
locationStatus.textContent = 'Ubicación: ✓';
|
||||||
|
locationStatus.style.backgroundColor = 'rgba(0, 128, 0, 0.7)'; // Green
|
||||||
|
updateSendButtonState();
|
||||||
|
|
||||||
|
// Once we get a good reading, we can stop watching
|
||||||
|
if (position.coords.accuracy < 100) { // If accuracy is under 100 meters
|
||||||
|
navigator.geolocation.clearWatch(watchId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error getting location:', error);
|
||||||
|
locationStatus.textContent = 'Error de ubicación';
|
||||||
|
locationStatus.style.backgroundColor = 'rgba(255, 0, 0, 0.7)'; // Red
|
||||||
|
|
||||||
|
// Try getting location once more with different options
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
document.getElementById('latitude').value = position.coords.latitude;
|
||||||
|
document.getElementById('longitude').value = position.coords.longitude;
|
||||||
|
hasLocation = true;
|
||||||
|
locationStatus.textContent = 'Ubicación: ✓';
|
||||||
|
locationStatus.style.backgroundColor = 'rgba(0, 128, 0, 0.7)'; // Green
|
||||||
|
updateSendButtonState();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Final error getting location:', error);
|
||||||
|
locationStatus.textContent = 'Error de ubicación';
|
||||||
|
},
|
||||||
|
{ maximumAge: 60000, timeout: 10000 }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 10000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Geolocation not supported in this browser.');
|
||||||
|
locationStatus.textContent = 'GPS no soportado';
|
||||||
|
locationStatus.style.backgroundColor = 'rgba(255, 0, 0, 0.7)'; // Red
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{% extends 'secondbase.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.js"></script>
|
||||||
|
|
||||||
@ -7,59 +7,148 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="padding-top: 30px;">
|
<div class="container-fluid" id="dash-container" style="background-color: rgba(189, 216, 3, 0.9); padding: 3rem; padding-top:10rem;display:block">
|
||||||
<h3>Hola <span style="color: darkgreen;">{{current_user.username}}</span></h3>
|
<h3>Hola <span style="color: darkgreen;">{{current_user.username}}</span></h3>
|
||||||
<p>Aquí puedes ver los pines que has agregado y ver tu enlace/QR de invitación. Cada vez que recargas esta página, tu enlace de invitacón cambia y el anterior se vuelve inválido.</p>
|
<p>Aquí puedes ver los pines que has agregado y consultar tu enlace/QR de invitación. Cada vez que recargas esta página, tu enlace de invitacón cambia y el anterior se vuelve inválido.</p>
|
||||||
<div id="qrgen" style="border-width:5px; border-color:#e6e6fa">
|
<div class="grid" style="padding-bottom: 2rem;">
|
||||||
</div>
|
<div id="qrgen" style="display:flex;justify-content: center;">
|
||||||
<script type="text/javascript">
|
</div>
|
||||||
var qrcode = new QRCode(document.getElementById("qrgen"), {
|
<script type="text/javascript">
|
||||||
text: "https://baches.qro.mx/registrame/{{invite_code}}",
|
var qrcode = new QRCode(document.getElementById("qrgen"), {
|
||||||
width: 128,
|
text: "https://baches.qro.mx/registrame/{{invite_code}}",
|
||||||
height: 128,
|
width: 256,
|
||||||
colorDark : "#000000",
|
height: 256,
|
||||||
colorLight : "#e6e6fa",
|
colorDark : "#000000",
|
||||||
correctLevel : QRCode.CorrectLevel.L
|
colorLight : "#e6e6fa",
|
||||||
});
|
correctLevel : QRCode.CorrectLevel.L
|
||||||
</script>
|
});
|
||||||
<h3>Tu link es:</h3>
|
</script>
|
||||||
<h2>https://baches.qro.mx/registrame/{{invite_code}}</h2>
|
<div style="display: grid; justify-content: center;">
|
||||||
|
<input type="text" id="invite_code" value="{{invite_code}}" style="" hidden="true">
|
||||||
|
<button id="copy-button" onclick="copyToClipboard()" style="background-color: rgb(255, 213, 0); color: rgb(0, 0, 0); border: none; padding: 12px 20px; border-radius: 25px; cursor: pointer; margin-top: 15px; display: flex; align-items: center; justify-content: center; font-size:1.2rem;font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.3s ease;">
|
||||||
|
<span style="font-size: 2rem; margin-right: 10px;">📋</span> Copia tu link de invitación
|
||||||
|
</button>
|
||||||
|
<script>
|
||||||
|
function copyToClipboard() {
|
||||||
|
var copyText = document.getElementById("invite_code");
|
||||||
|
copyText.select();
|
||||||
|
// Modern clipboard API
|
||||||
|
navigator.clipboard.writeText("https://baches.qro.mx/registrame/" + copyText.value).then(() => {
|
||||||
|
console.log('Link copied successfully');
|
||||||
|
}).catch(err => {
|
||||||
|
// Fallback for browsers that don't support clipboard API
|
||||||
|
document.execCommand("copy");
|
||||||
|
console.log('Using fallback clipboard method');
|
||||||
|
});
|
||||||
|
|
||||||
|
var button = document.getElementById("copy-button");
|
||||||
|
|
||||||
|
// Change to copied state
|
||||||
|
button.style.backgroundColor = "green";
|
||||||
|
button.style.color = "white";
|
||||||
|
button.style.transition = "all 0.5s ease";
|
||||||
|
button.innerHTML = "<span style=\"font-size: 2rem; margin-right: 10px;\">✓</span> ¡Copiado!";
|
||||||
|
|
||||||
|
// Revert back after 2 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
button.style.backgroundColor = "rgb(255, 213, 0)";
|
||||||
|
button.style.color = "rgb(0, 0, 0)";
|
||||||
|
button.style.transition = "all 0.5s ease";
|
||||||
|
button.innerHTML = "<span style=\"font-size: 2rem; margin-right: 10px;\">📋</span> Copia tu link de invitación";
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<p>¡Cada vez que recargas esta página, tu código de invitación cambia para prevenir malos usos! </p>
|
||||||
|
<p>Asegúrate de que a quien invites use su invitación de inmediato 😈</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if pins %}
|
{% if pins %}
|
||||||
{% for pin in pins %}
|
{% for pin in pins %}
|
||||||
|
<div class="pin-card" style="{% if pin.reviewed and pin.reviewed == false %} background-color: rgb(255, 170, 0) {% endif %};margin-bottom: 2rem; background-color: #f8f9fa; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; display: flex; flex-wrap: wrap;">
|
||||||
|
<div class="pin-image" style="flex: 0 0 250px; padding: 15px; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<img style="max-width: 100%; max-height: 200px; object-fit: cover; border-radius: 4px;" src="{{ pin.photo }}" alt="Foto de mejora urbana">
|
||||||
|
</div>
|
||||||
|
<div class="pin-data" style="flex: 1; padding: 15px;">
|
||||||
|
<h4 style="margin-top: 0; color: #333;">{{ pin.typeofpin }}</h4>
|
||||||
|
<p><strong>Agregado:</strong> {{ pin.time }}</p>
|
||||||
|
{% if pin.reviewed and pin.reviewed == False %}<p><strong>Estado:</strong> Pendiente de revisión</p>{% endif %}
|
||||||
|
<p><strong>Modificado:</strong> {% if pin.last_mod %} {{pin.last_mod}} {% endif %}</p>
|
||||||
|
<p><strong>Descripción:</strong> {{ pin.description }}</p>
|
||||||
|
<p><strong>Dirección:</strong> {{ pin.address }}</p>
|
||||||
|
<p><small>Coordenadas: {{ pin.lat }}, {{ pin.lng }}</small></p>
|
||||||
|
<div class="pin-actions" style="margin-top: 15px;">
|
||||||
|
<button type="button" onclick="openEditModal('{{pin._id}}')" style="background-color: #007bff; color: white; border: none; padding: 6px 12px; border-radius: 4px; margin-right: 8px; cursor: pointer;">Editar</button>
|
||||||
|
<a href="/remove_pin/{{pin._id}}"><button type="button" style="background-color: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer;">Eliminar</button></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table border="1" cellpadding="5" cellspacing="0">
|
<!-- Modal para editar pin -->
|
||||||
|
<div id="editModal{{pin._id}}" class="modal" style="display:none; position:fixed; z-index:1000; left:0; top:0; width:100%; height:100%; background-color:rgba(0,0,0,0.4);">
|
||||||
|
<div class="modal-content" style="background-color:#fff; margin:10% auto; padding:20px; width:70%; border-radius:5px;">
|
||||||
|
<span class="close" onclick="closeEditModal('{{pin._id}}')" style="color:#aaa; float:right; font-size:28px; font-weight:bold; cursor:pointer;">×</span>
|
||||||
|
<h2>Editar Pin</h2>
|
||||||
|
<form action="/edit_pin/{{pin._id}}" method="post">
|
||||||
|
<div style="margin-bottom:15px;">
|
||||||
|
<label for="typeofpin{{pin._id}}"><b>Tipo de Mejora Urbana:</b></label>
|
||||||
|
<select id="typeofpin{{pin._id}}" name="typeofpin" style="width:100%; padding:8px;">
|
||||||
|
|
||||||
<tbody>
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:15px;">
|
||||||
|
<label for="fixed{{pin._id}}"><b>¿Está solucionado?</b></label>
|
||||||
|
<select id="fixed{{pin._id}}" name="fixed" style="width:100%; padding:8px;">
|
||||||
|
<option value="True" {% if pin.fixed %}selected{% endif %}>Sí, está solucionado</option>
|
||||||
|
<option value="False" {% if not pin.fixed %}selected{% endif %}>No, sigue pendiente</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:15px;">
|
||||||
|
<label for="description{{pin._id}}"><b>Descripción:</b></label>
|
||||||
|
<textarea id="description{{pin._id}}" name="description" style="width:100%; padding:8px; height:100px;">{{pin.description}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:15px;">
|
||||||
|
<label for="address{{pin._id}}"><b>Dirección:</b></label>
|
||||||
|
<input type="text" id="address{{pin._id}}" name="address" value="{{pin.address}}" style="width:100%; padding:8px;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" style="background-color:#4CAF50; color:white; padding:10px 15px; border:none; cursor:pointer; border-radius:4px;">Guardar cambios</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tr>
|
<script type="text/javascript">
|
||||||
<td>Agregado el</td>
|
|
||||||
<td>{{ pin.time }}</td>
|
var iconTypes2 = {
|
||||||
</tr>
|
'bache': '⚠️',
|
||||||
<tr>
|
'coladera': '⭕',
|
||||||
<td>Photo</td>
|
'obra sin terminar': '🔧',
|
||||||
<td><img style="max-height: 66px;" src="{{ pin.photo }}"></td>
|
'escombro': '🗑️',
|
||||||
</tr>
|
'robo-asalto': '💀',
|
||||||
<tr>
|
'biciestacionamiento': '🏁',
|
||||||
<td>Latitude</td>
|
'mala iluminación': '💡',
|
||||||
<td>{{ pin.lat }}</td>
|
'bici blanca': '🚲',
|
||||||
</tr>
|
'zapato blanco': '👟',
|
||||||
<tr>
|
};
|
||||||
<td>Longitude</td>
|
function openEditModal(id) {
|
||||||
<td>{{ pin.lng }}</td>
|
document.getElementById('editModal'+id).style.display = 'block';
|
||||||
</tr>
|
}
|
||||||
<tr>
|
|
||||||
<td>Tipo de mejora urbana</td>
|
function closeEditModal(id) {
|
||||||
<td>{{ pin.typeofpin }}</td>
|
document.getElementById('editModal'+id).style.display = 'none';
|
||||||
</tr>
|
}
|
||||||
<tr>
|
</script>
|
||||||
<td>Eliminar pin</td>
|
|
||||||
<td><a href="/remove_pin/{{pin._id}}"><button type="submit" formmethod="get">Eliminar</button></a></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
const typeofpins = document.getElementsByName('typeofpin');
|
||||||
|
for (let i = 0; i < typeofpins.length; i++) {
|
||||||
|
for (const iconType in iconTypes2) {
|
||||||
|
const capitalizedIconType = iconType.charAt(0).toUpperCase() + iconType.slice(1);
|
||||||
|
typeofpins[i].innerHTML += '<option value="'+iconType+'">'+iconTypes2[iconType]+' '+capitalizedIconType+'</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{% extends 'secondbase.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section>
|
<section>
|
||||||
{% if message %}
|
{% if message %}
|
||||||
|
|||||||
17
templates/faq.html
Normal file
17
templates/faq.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<article id="preguntas">
|
||||||
|
<h4>Preguntas Frecuentes</h4>
|
||||||
|
<h5>¿Qué es Bachemapa?</h5>
|
||||||
|
<p>Bachemapa es una plataforma dedicada a documentar y reportar problemas con el desarrollo urbano en Querétaro, México. Encuentra y aporta información actualizada sobre baches, deficiencias en infraestructura y más.</p>
|
||||||
|
<h5>¿Por qué no puedo registrarme?</h5>
|
||||||
|
<p>Para mantener la integridad de nuestra comunidad y asegurar un entorno de cooperación y compromiso, hemos optado por permitir el registro sólo mediante invitación. Esta modalidad nos ayuda a garantizar que cada nuevo integrante aporte valor a la experiencia colectiva, fomentando un ambiente seguro y centrado en el desarrollo urbano responsable.</p>
|
||||||
|
<h5>¿Cómo puedo obtener una invitación?</h5>
|
||||||
|
<p>Para obtener una invitación, sólo tienes que pedírsela a un amigo que ya esté registrado en Bachemapa. Cada usuario registrado tiene un enlace de invitación que puede compartir con sus amigos. Si no conoces a nadie que esté registrado, puedes preguntarle a tu amix de confianza, o acercarte a nosotrxs en nuestro <a href="https://t.me/bachemapa">grupo de Telegram</a>.</p>
|
||||||
|
<h5>¿Cómo puedo reportar un bache?</h5>
|
||||||
|
<p>Para reportar un bache, sólo tienes que seleccionar el lugar donde está (o aceptar la geolocalización al abrir la página), hacer clic en el botón "Agregar" en la parte superior de la página. Agrega una foto, selecciona el tipo de (des)mejora urbana y una descripción poéticamente responsable y listo. Tu reporte será visible en el mapa en cuanto lo envíes.</p>
|
||||||
|
<h5>¿Cómo puedo ver los reportes que he hecho?</h5>
|
||||||
|
<p>Para ver los reportes que has hecho, sólo tienes que hacer clic en el botón "Perfil" en la parte superior de la página. Ahí podrás ver todos los reportes que has hecho y elmininarlos, así como consultar tu enlace y tu código de invitación.</p>
|
||||||
|
<h5>¿Cómo puedo invitar a mis amigos a reportar baches?</h5>
|
||||||
|
<p>Para invitar a tus amigos a reportar baches, sólo tienes que compartir tu enlace de invitación con ellos o dejar que escaneen el código QR en tu Perfil (ese código es equivalente al enlace). El enlace expira rápido, en cuanto recargas el perfil, así que insta a tus amix a que se registren frente a tu cara, con tus bellos ojos de testigas.</p>
|
||||||
|
<h5>¿Cómo puedo subir en el ranking de Bachistas?</h5>
|
||||||
|
<p>Para subir en el ranking de Bachistas, sólo tienes que reportar más de tus baches favoritos y más seguido.</p>
|
||||||
|
</article>
|
||||||
3
templates/footer.html
Normal file
3
templates/footer.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<footer>
|
||||||
|
<p>Desarrollado por Kernel Panic Room, dominio patrocinado por el programa RSE de <a href="https://qro.mx">Qro.mx</a>. Si tienes dudas, comentarios, recomendaciones o quieres reportar un error, únete a nuestro canal de Telegram <a href="https://t.me/bachemapa">aquí</a></p>
|
||||||
|
</footer>
|
||||||
@ -1,12 +1,18 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="map" style="height: 100%; position: absolute;"></div>
|
<div id="map" style="height: 100%; position: static; cursor:crosshair"></div>
|
||||||
<script>
|
<script>
|
||||||
var map = new L.map('map', {zoomControl: false}, center=([20.57, -100.38], zoom=16));
|
// document.addEventListener('DOMContentLoaded', function() {
|
||||||
var user_marker = new L.marker([20.57, -100.38]);
|
var map = new L.map('map', {center: [20.57, -100.38], zoom:16, zoomControl: false });
|
||||||
var user_radial = new L.circle(L.circle(user_marker.latlng));
|
console.warn("Mapa creado");
|
||||||
|
// Initialize user_marker with a default position
|
||||||
|
var user_marker = new L.marker([20.57, -100.38]).addTo(map);
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
getLocation();
|
||||||
|
});
|
||||||
|
var user_radial = new L.circle([20.57, -100.38], {radius:200}).addTo(map);
|
||||||
|
console.warn("Mapa cargado");
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© OpenStreetMap contributors',
|
attribution: '© OpenStreetMap contributors',
|
||||||
detectRetina: true,
|
detectRetina: true,
|
||||||
@ -17,67 +23,138 @@
|
|||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
//types of markers
|
//types of markers
|
||||||
var iconTypes = {
|
var iconTypes = {
|
||||||
'bache': '<i class="fas fa-exclamation-triangle"></i>',
|
'bache': '<i class="indicator fas fa-exclamation-triangle"></i>',
|
||||||
'coladera': '<i class="fas fa-ring"></i>',
|
'coladera': '<i class="indicator fas fa-ring"></i>',
|
||||||
'obra sin terminar': '<i class="fas fa-wrench"></i>',
|
'obra sin terminar': '<i class="indicator fas fa-wrench"></i>',
|
||||||
'escombro': '<i class="far fa-trash-alt"></i>',
|
'escombro': '<i class="indicator far fa-trash-alt"></i>',
|
||||||
'robo-asalto': '<i class="fas fa-skull"></i>',
|
'robo-asalto': '<i class="indicator fas fa-skull"></i>',
|
||||||
'biciestacionamiento': '<i class="fas fa-flag-checkered"></i>',
|
'biciestacionamiento': '<i class="indicator fas fa-flag-checkered"></i>',
|
||||||
'mala iluminación': '<i class="far fa-lightbulb"></i>',
|
'mala iluminación': '<i class="indicator far fa-lightbulb"></i>',
|
||||||
'bici blanca': '<i style="color:white" class="fas fa-bicycle"></i>',
|
'bici blanca': '<i style="color:white" class="indicator fas fa-bicycle"></i>',
|
||||||
'zapato blanco': '<i style="color:white" class="fas fa-shoe-prints"></i>',
|
'zapato blanco': '<i style="color:white" class="indicator fas fa-shoe-prints"></i>',
|
||||||
};
|
};
|
||||||
var markerCluster = L.markerClusterGroup();
|
|
||||||
|
|
||||||
//new markerCluster = window.L.MarkerClusterGroup();
|
|
||||||
{% for pin in pins %}
|
|
||||||
|
|
||||||
var icon = L.divIcon({
|
// Improved geolocation using direct browser API
|
||||||
className: '{{pin.typeofpin}}',
|
function getLocation() {
|
||||||
html: iconTypes['{{pin.typeofpin}}'],
|
if (navigator.geolocation) {
|
||||||
iconSize:[10,10],
|
console.log("Geolocalización soportada en este navegador.", navigator.geolocation);
|
||||||
iconAnchor:[5,5],
|
navigator.geolocation.getCurrentPosition(
|
||||||
});
|
onLocationFound,
|
||||||
markerCluster.addLayer(L.marker([{{ pin.lat }}, {{ pin.lng }}], {icon: icon}).bindPopup("<b>{{ pin.description }}</b>{% if pin.address %}<br>{{pin.address}}{% endif %}<p>Fecha del reporte: {{pin.time}}</p><br><img src='{{ pin.photo }}'>"));
|
onLocationError,
|
||||||
|
{
|
||||||
//markerCluster.addlayer(
|
enableHighAccuracy: true,
|
||||||
//L.marker([{{ pin.lat }}, {{ pin.lng }}], {icon: icon}).addTo(map)
|
timeout: 10000,
|
||||||
// .bindPopup("<b>{{ pin.description }}</b><p>Fecha del reporte: {{pin.time}}</p><br><img src='{{ pin.photo }}'>");
|
maximumAge: 0
|
||||||
{% endfor %}
|
}
|
||||||
map.addLayer(markerCluster);
|
);
|
||||||
|
} else {
|
||||||
|
alert("La geolocalización no está soportada en este navegador.");
|
||||||
|
map.setView([20.57, -100.38], 13); // Default to Querétaro's coordinates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Geolocation function
|
// Geolocation function
|
||||||
function onLocationFound(e) {
|
function onLocationFound(position) {
|
||||||
var radius = e.accuracy / 2; // Accuracy of the location
|
console.warn("Geolocalización exitosa:", position);
|
||||||
|
let lat = position.coords.latitude.toFixed(6);
|
||||||
|
let lng = position.coords.longitude.toFixed(6);
|
||||||
|
let fullCoords = position.coords;
|
||||||
|
console.warn("Ubicación detectada:", fullCoords);
|
||||||
|
console.warn("Latitud:", lat, "Longitud:", lng);
|
||||||
|
let latlng = L.latLng(parseFloat(lat), parseFloat(lng));
|
||||||
|
console.warn("Ubicación detectada:", latlng);
|
||||||
|
let radius = position.coords.accuracy / 2; // Accuracy of the location
|
||||||
|
|
||||||
// Add a pin (marker) at the user's location
|
// Add a pin (marker) at the user's location
|
||||||
user_marker = L.marker(e.latlng).addTo(map)
|
user_marker.setLatLng(latlng).bindPopup("Te detectamos en un radio de " + parseInt(radius) + " metros de este punto").openPopup();
|
||||||
.bindPopup("Te detectamos en un radio de " + parseInt(radius) + " metros de este punto").openPopup();
|
|
||||||
document.getElementById('lat').value = e.latlng.lat;
|
|
||||||
document.getElementById('lng').value = e.latlng.lng;
|
|
||||||
// Add a circle around the user's location
|
// Add a circle around the user's location
|
||||||
user_radial = L.circle(e.latlng, radius).addTo(map);
|
user_radial.remove(); // Remove existing radius
|
||||||
|
user_radial = L.circle(latlng, radius).addTo(map);
|
||||||
|
console.warn("latlng:", latlng, "radio:", radius);
|
||||||
// Center the map on the user's location
|
// Center the map on the user's location
|
||||||
map.setView(e.latlng, 18); // Adjust zoom level as needed
|
map.setView(latlng, 16); // Adjust zoom level as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error handling for geolocation
|
// Error handling for geolocation
|
||||||
function onLocationError(e) {
|
function onLocationError(error) {
|
||||||
alert(e.message);
|
let errorMsg;
|
||||||
|
switch(error.code) {
|
||||||
|
case error.PERMISSION_DENIED:
|
||||||
|
errorMsg = "Usuario denegó la solicitud de geolocalización.";
|
||||||
|
break;
|
||||||
|
case error.POSITION_UNAVAILABLE:
|
||||||
|
errorMsg = "La información de ubicación no está disponible.";
|
||||||
|
break;
|
||||||
|
case error.TIMEOUT:
|
||||||
|
errorMsg = "Se agotó el tiempo de espera para la solicitud de ubicación.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMsg = "Ocurrió un error desconocido al obtener tu ubicación.";
|
||||||
|
}
|
||||||
|
console.error("Error de geolocalización:", errorMsg);
|
||||||
|
alert(errorMsg);
|
||||||
|
map.setView([20.57, -100.38], 13); // Default to Querétaro's coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Leaflet's built-in location detection
|
// Start location detection
|
||||||
map.on('locationfound', onLocationFound);
|
|
||||||
map.on('locationerror', onLocationError);
|
|
||||||
|
|
||||||
// Start the location detection
|
|
||||||
map.locate({setView: true, zoom: 16});
|
|
||||||
map.on('click', function(e) {
|
map.on('click', function(e) {
|
||||||
var latlng = e.latlng;
|
console.warn("Click detectado en el mapa:", e);
|
||||||
document.getElementById('lat').value = latlng.lat;
|
let latlng = e.latlng;
|
||||||
document.getElementById('lng').value = latlng.lng;
|
// Check if elements exist before accessing
|
||||||
map.flyTo(e.latlng, zoom=18);
|
if (document.getElementById('lat') && document.getElementById('lng')) {
|
||||||
user_marker.setLatLng(e.latlng).bindPopup('<button id="pinner-pop">Agregar bache-o-cosa</button>').openPopup();
|
document.getElementById('lat').value = latlng.lat;
|
||||||
|
document.getElementById('lng').value = latlng.lng;
|
||||||
|
}
|
||||||
|
latlng.lat = parseFloat(latlng.lat).toFixed(6);
|
||||||
|
latlng.lng = parseFloat(latlng.lng).toFixed(6);
|
||||||
|
console.warn("Latitud:", latlng.lat, "Longitud:", latlng.lng);
|
||||||
|
destination = L.latLng(latlng.lat, latlng.lng);
|
||||||
|
// Calculate the vertical offset (25% of the map's height)
|
||||||
|
let offset = map.getSize().y * 0.25;
|
||||||
|
// Convert the clicked location to container point
|
||||||
|
let containerPoint = map.latLngToContainerPoint(latlng);
|
||||||
|
// Adjust the container point upward by the offset
|
||||||
|
let targetCenterPoint = L.point(containerPoint.x, containerPoint.y - offset);
|
||||||
|
// Convert the adjusted point back to geographical coordinates
|
||||||
|
let targetLatLng = map.containerPointToLatLng(targetCenterPoint);
|
||||||
|
|
||||||
|
|
||||||
|
user_marker.setLatLng(latlng).bindPopup('<button id="pinner-pop">Agregar bache-o-cosa</button>').openPopup();
|
||||||
|
// Fly to the adjusted center so the marker appears 25% lower from the center
|
||||||
|
map.flyTo(targetLatLng, 17, { duration: 0.7 });
|
||||||
|
|
||||||
|
// map.once('moveend', function() {
|
||||||
|
|
||||||
|
// });
|
||||||
user_radial.remove();
|
user_radial.remove();
|
||||||
});
|
});
|
||||||
|
// });
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
console.log("Pins data:", {{ pins | tojson }});
|
||||||
|
let markerCluster = L.markerClusterGroup();
|
||||||
|
{% for pin in pins %}
|
||||||
|
|
||||||
|
var icon = L.divIcon({
|
||||||
|
className: '{{pin.typeofpin}}',
|
||||||
|
html: iconTypes['{{pin.typeofpin}}'],
|
||||||
|
iconSize: [10,10],
|
||||||
|
iconAnchor: [5,5]
|
||||||
|
});
|
||||||
|
let p{{ pin._id }} = L.marker([{{ pin.lat }}, {{ pin.lng }}], { icon: icon })
|
||||||
|
.bindPopup("<b>{{ pin.description }}</b><p>Fecha del reporte: {{pin.time}}</p><br><img src='{{ pin.photo }}'>");
|
||||||
|
markerCluster.addLayer(p{{ pin._id }});
|
||||||
|
{% endfor %}
|
||||||
|
map.addLayer(markerCluster);
|
||||||
|
</script>
|
||||||
|
</script>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div style="position: absolute; bottom: 1rem; left: 50%; transform: translateX(-50%); z-index: 999">
|
||||||
|
<button class="control-button" onclick="window.location.href='/camera'"><i class="fas fa-camera"></i></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
97
templates/leaderboard.html
Normal file
97
templates/leaderboard.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container" style="background-color: rgba(255,255,255,0.85); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(15px); padding:2rem; margin-top: 5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header-cta">
|
||||||
|
<h2 style="text-align: center; color: #172f01; margin-bottom: 1rem;">¡Hazte mapista y pon tu granito de fotoevidencia!</h2>
|
||||||
|
<p style="text-align: center; margin-bottom: 2rem;">Cada contribución cuenta. Mira lo que estamos logrando y sé parte del cambio.<br></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid container for side-by-side layout -->
|
||||||
|
<div class="grid">
|
||||||
|
<!-- Table column -->
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; align-items: center; height: 300px;">
|
||||||
|
<table id="mapistas" style="color: rgba(202,216,3,0.7); width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr style="color: rgba(202,216,3,0.7);">
|
||||||
|
<th scope="col">Mapista</th>
|
||||||
|
<th scope="col">Marcador</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for leader in leaders %}
|
||||||
|
<tr style="color: rgba(202,216,3,0.7);">
|
||||||
|
<td scope="row">{{leader.username}}</td>
|
||||||
|
<td scope="row">{{leader.count}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart column -->
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<h3>Porcentaje de usuarios activos</h3>
|
||||||
|
<div class="chart-container" style="display:block ruby; position: relative; height:300px; width:100%; margin: 0 auto;">
|
||||||
|
<canvas id="activeUsersChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const ctx = document.getElementById('activeUsersChart').getContext('2d');
|
||||||
|
const activePercentage = {{ percentage }};
|
||||||
|
const inactivePercentage = 100 - activePercentage;
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
// Append percentages directly to the legend labels
|
||||||
|
labels: [
|
||||||
|
'Usuarios Activos (' + activePercentage + '%)',
|
||||||
|
'Usuarios Inactivos (' + inactivePercentage + '%)'
|
||||||
|
],
|
||||||
|
datasets: [{
|
||||||
|
data: [activePercentage, inactivePercentage],
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(202,216,3,0.7)',
|
||||||
|
'rgba(87, 90, 91, 0.8)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(202,216,3,0.7)',
|
||||||
|
'rgb(87, 90, 91)'
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Distribución de Usuarios Activos'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
var label = context.label || '';
|
||||||
|
var value = context.parsed;
|
||||||
|
return label + ': ' + value + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,19 +1,52 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
{% extends 'secondbase.html' %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container" style="display: flex; justify-content: center; align-items: center; min-width: 40vw; flex-direction: column;">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
form {
|
form {
|
||||||
|
max-width: 80vw;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
<form method="post" enctype="multipart/form-data" style="background-color: rgb(205, 243, 148);">
|
|
||||||
{{ form.csrf_token }}
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
{{ form.username.label }}<br>
|
|
||||||
{{ form.username() }}
|
|
||||||
{{ form.pwd.label }}
|
|
||||||
{{ form.pwd() }}
|
|
||||||
|
|
||||||
<p>{{ form.submit() }}</p>
|
.flash-message {
|
||||||
|
position: fixed;
|
||||||
|
top: 20vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 100;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div id="flash-message" class="flash-message">
|
||||||
|
{{ messages }}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
var flashMessage = document.getElementById('flash-message');
|
||||||
|
if (flashMessage) {
|
||||||
|
flashMessage.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" style="background-color: rgb(205, 243, 148);">
|
||||||
|
{{ login_form.csrf_token }}
|
||||||
|
{{ login_form.hidden_tag() }}
|
||||||
|
{{ login_form.username.label }}
|
||||||
|
{{ login_form.username() }}
|
||||||
|
{{ login_form.pwd.label }}
|
||||||
|
{{ login_form.pwd() }}
|
||||||
|
|
||||||
|
<p>{{ login_form.submit() }}</p>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
75
templates/nav.html
Normal file
75
templates/nav.html
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<nav style="height: auto; align-items: center;">
|
||||||
|
<ul>
|
||||||
|
<a href="/">
|
||||||
|
<li style="display: flex; align-items: center;">
|
||||||
|
<img id="logo" src="{{ url_for('static', filename='images/bachemapalogo.png') }}" alt="Bachemapa Logo" style="height: 10vh; margin-right: 10px;">
|
||||||
|
<h3 style="margin-bottom: 0;" id="sitename"><span style="color: #4a8522;">Bache</span><span style="color: #fa8721;">mapa</span></h3>
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<!-- Mobile menu toggle -->
|
||||||
|
<!-- Mobile menu toggle (only appears on screens less than 600px wide) -->
|
||||||
|
<details role="list" dir="rtl" class="mobile-menu" style="margin:0; margin-right:5rem; position: relative;">
|
||||||
|
<summary aria-haspopup="listbox" role="link" class="secondary" style="transition: transform 0.3s ease;">
|
||||||
|
<i style="color:white" class="fas fa-bars"></i>
|
||||||
|
</summary>
|
||||||
|
<ul role="listbox" style="position: absolute; right: 0; top: 100%; transition: transform 0.3s ease; transform: translateX(0);">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li><a id="pinner-top" href="#" role="button"><i class="fas fa-plus"></i></a></li>
|
||||||
|
<li><a href="/dashboard" role="button"><i class="fas fa-user"></i></a></li>
|
||||||
|
<li><a href="/leaderboard" role="button"><i class="fas fa-trophy"></i></a></li>
|
||||||
|
<li><a href="/quienes" role="button" class="secondary"><i class="fas fa-info-circle"></i></a></li>
|
||||||
|
<li><a href="/logout" role="button" class="secondary"><i class="fas fa-sign-out-alt"></i></a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="/thelogin" role="button"><i class="fas fa-sign-in-alt"></i></a></li>
|
||||||
|
<li><a href="/leaderboard" role="button"><i class="fas fa-trophy"></i></a></li>
|
||||||
|
<li><a href="/quienes" role="button" class="secondary"><i class="fas fa-info-circle"></i></a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Desktop menu (only appears on screens 600px or wider) -->
|
||||||
|
<ul class="desktop-menu">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li><a id="pinner-top-desktop" href="#" role="button">Agregar</a></li>
|
||||||
|
<li><a href="/logout" role="button" class="secondary">Cerrar Sesión</a></li>
|
||||||
|
<li><a href="/dashboard" role="button">Perfil</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="/thelogin" role="button">Iniciar Sesión</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
|
||||||
|
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 810px) {
|
||||||
|
.desktop-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#sitename {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 811px) {
|
||||||
|
.mobile-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#sitename {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-image: url("{{ url_for('static', filename='images/bg-trees.jpg') }}");
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- FAQ button is absolutely positioned -->
|
||||||
|
<li style="display: block; position: absolute; right: 2rem;bottom:2rem; z-index:1200"><a style="border-radius: 100%; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; margin-right: 10px; text-decoration: none;" href="/quienes#tengopreguntas"><i style="color:white" class="fas fa-question-circle"></i></a></li>
|
||||||
@ -1,17 +1,35 @@
|
|||||||
{% extends 'secondbase.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container" style="max-width: 80vw; max-height: 80vh; margin: 2rem auto; overflow-y: auto; padding: 1rem;">
|
||||||
<div style="padding: 15px; overflow-y: scroll;">
|
<div class="info" style="margin-bottom: 2rem;">
|
||||||
<h4>Somos civiles y voluntari@s</h4>
|
<h2 style="text-align: center;">Somos civiles y voluntari@s</h4>
|
||||||
<p>Esta página es un servicio comunitario desarrollado por el hackspace Kernel Panic Room, con un (maravilloso) dominio patrocinado por el programa de RSE de <a href="https://qro.mx">Qro.mx</a>.</p>
|
<p>Esta página es un servicio comunitario desarrollado por el hackspace Kernel Panic Room, con un (maravilloso) dominio patrocinado por el programa de RSE de <a href="https://qro.mx">Qro.mx</a>.</p>
|
||||||
<p>Nuestro objetivo es construir un puente entre la ciudadanía y las autoridades estatales y municipales para poder recabar, catalogar y visibilizar los problemas infraestructurales de nuestra entidad, con el fin de facilitar que se solucionen.</p>
|
<p>Nuestro objetivo es construir un puente entre la ciudadanía y las autoridades estatales y municipales para poder recabar, catalogar y visibilizar los problemas infraestructurales de nuestra entidad, con el fin de facilitar que se solucionen.</p>
|
||||||
|
|
||||||
<p>No perseguimos fines políticos, sólo estamos interesad@s en mejorar nuestro entorno y la calidad de vida que tenemos.</p>
|
<p>No perseguimos fines políticos, sólo estamos interesad@s en mejorar nuestro entorno y la calidad de vida que tenemos.</p>
|
||||||
|
|
||||||
<h2>Este mapa no hubiera sido posible si no hubieramos conocido a José Luis Ramos</h2>
|
<h3 style="text-align: center;">Este mapa no hubiera sido posible si no hubieramos conocido a José Luis Ramos</h2>
|
||||||
<p>José Luis falleció en Marzo del 2024. No sólo se nos fue una persona maravillosa, sino también el activista más activo que much@s de nosotr@s conocimos. Este mapa es un homenaje a su vida y a la lucha que encabezó siempre buscando hacer del mundo un lugar mejor para <b>tod@s</b>. Sí, incluso para quienes nunca lo entendieron a él o a su lucha.</p>
|
<p>José Luis falleció en Marzo del 2024. No sólo se nos fue una persona maravillosa, sino también el activista más activo que much@s de nosotr@s conocimos. Este mapa es un homenaje a su vida y a la lucha que encabezó siempre buscando hacer del mundo un lugar mejor para <b>tod@s</b>. Sí, incluso para quienes nunca lo entendieron a él o a su lucha.</p>
|
||||||
<p>Nos volveremos a ver, amigo José Luis, y rodaremos juntos de nuevo. Esperamos que te estés riendo desde las alturas de tu vuelo.</p>
|
<p>Nos volveremos a ver, amigo José Luis, y rodaremos juntos de nuevo. Esperamos que te estés riendo desde las alturas de tu vuelo.</p>
|
||||||
|
|
||||||
<img src="{{url_for('static', filename='images/ramos.jpg')}}">
|
<img src="{{url_for('static', filename='images/ramos.jpg')}}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info" id="tengopreguntas">
|
||||||
|
<h4>Preguntas Frecuentes</h4>
|
||||||
|
<h5>¿Qué es Bachemapa?</h5>
|
||||||
|
<p>Bachemapa es una plataforma dedicada a documentar y reportar problemas con el desarrollo urbano en Querétaro, México. Encuentra y aporta información actualizada sobre baches, deficiencias en infraestructura y más.</p>
|
||||||
|
<h5>¿Por qué no puedo registrarme?</h5>
|
||||||
|
<p>Para mantener la integridad de nuestra comunidad y asegurar un entorno de cooperación y compromiso, hemos optado por permitir el registro sólo mediante invitación. Esta modalidad nos ayuda a garantizar que cada nuevo integrante aporte valor a la experiencia colectiva, fomentando un ambiente seguro y centrado en el desarrollo urbano responsable.</p>
|
||||||
|
<h5>¿Cómo puedo obtener una invitación?</h5>
|
||||||
|
<p>Para obtener una invitación, sólo tienes que pedírsela a un amigo que ya esté registrado en Bachemapa. Cada usuario registrado tiene un enlace de invitación que puede compartir con sus amigos. Si no conoces a nadie que esté registrado, puedes preguntarle a tu amix de confianza, o acercarte a nosotrxs en nuestro <a href="https://t.me/bachemapa">grupo de Telegram</a>.</p>
|
||||||
|
<h5>¿Cómo puedo reportar un bache?</h5>
|
||||||
|
<p>Para reportar un bache, sólo tienes que seleccionar el lugar donde está (o aceptar la geolocalización al abrir la página), hacer clic en el botón "Agregar" en la parte superior de la página. Agrega una foto, selecciona el tipo de (des)mejora urbana y una descripción poéticamente responsable y listo. Tu reporte será visible en el mapa en cuanto lo envíes.</p>
|
||||||
|
<h5>¿Cómo puedo ver los reportes que he hecho?</h5>
|
||||||
|
<p>Para ver los reportes que has hecho, sólo tienes que hacer clic en el botón "Perfil" en la parte superior de la página. Ahí podrás ver todos los reportes que has hecho y elmininarlos, así como consultar tu enlace y tu código de invitación.</p>
|
||||||
|
<h5>¿Cómo puedo invitar a mis amigos a reportar baches?</h5>
|
||||||
|
<p>Para invitar a tus amigos a reportar baches, sólo tienes que compartir tu enlace de invitación con ellos o dejar que escaneen el código QR en tu Perfil (ese código es equivalente al enlace). El enlace expira rápido, en cuanto recargas el perfil, así que insta a tus amix a que se registren frente a tu cara, con tus bellos ojos de testigas.</p>
|
||||||
|
<h5>¿Cómo puedo subir en el ranking de Bachistas?</h5>
|
||||||
|
<p>Para subir en el ranking de Bachistas, sólo tienes que reportar más de tus baches favoritos y más seguido.</p>
|
||||||
|
</div></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
{% extends 'secondbase.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Registro de nuevo usuario</h1>
|
<h1>Registro de nuevo usuario</h1>
|
||||||
<form method="post" enctype="multipart/form-data" style="background-color: rgb(205, 243, 148);">
|
<form method="post" enctype="multipart/form-data" style="background-color: rgb(205, 243, 148);">
|
||||||
{{ form.csrf_token }}
|
{{ rform.csrf_token }}
|
||||||
{{ form.hidden_tag() }}
|
{{ rform.hidden_tag() }}
|
||||||
{{ form.username.label }}<br>
|
{{ rform.username.label }}<br>
|
||||||
{{ form.username() }}
|
{{ rform.username() }}
|
||||||
{{ form.pwd.label }}
|
{{ rform.pwd.label }}
|
||||||
{{ form.pwd() }}
|
{{ rform.pwd() }}
|
||||||
|
|
||||||
<p>{{ form.submit() }}</p>
|
<p>{{ rform.submit() }}</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-theme="light" lang="es" style="background-color: rgb(205, 243, 148); color: black; overflow-y:scroll">
|
<html data-theme="light" lang="es" style="background-color: rgb(205, 243, 148); color: black; height: 100%;">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>Mapa interactivo de baches y otras cosas</title>
|
<title>Mapa interactivo de baches y otras cosas</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='pico.amber.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='pico.amber.css') }}">
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favico.ico') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
|
||||||
<meta name="description" content="Bachemapa es una plataforma dedicada a documentar y reportar problemas con el desarrollo urbano en Querétaro, México. Encuentra y aporta información actualizada sobre baches, deficiencias en infraestructura y más.">
|
<meta name="description" content="Bachemapa es una plataforma dedicada a documentar y reportar problemas con el desarrollo urbano en Querétaro, México. Encuentra y aporta información actualizada sobre baches, deficiencias en infraestructura y más.">
|
||||||
@ -32,40 +32,38 @@
|
|||||||
<link rel="canonical" href="https://baches.qro.mx">
|
<link rel="canonical" href="https://baches.qro.mx">
|
||||||
<!-- Versión pública 0.1 -->
|
<!-- Versión pública 0.1 -->
|
||||||
</head>
|
</head>
|
||||||
<body style="color: black;overflow-y: scroll;">
|
<body style="color: black; height: 100%; margin: 0;">
|
||||||
<nav style="height: auto; align-items:center">
|
{% include 'nav.html' %}
|
||||||
<ul>
|
|
||||||
<a href="/"><li><h3 style="margin-bottom: 0;">Bachemapa</h3></li></a>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><button id="pinner-top"><a href="/">Al mapa</a></button></li>
|
|
||||||
<li><button><a href="/quienes">Somos</a></button></li>
|
|
||||||
<li><button style="background-color: rgb(241, 165, 0);"><a href="/logout">Salir</a></button></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages() %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div style="position:sticky; z-index: 9999;">
|
|
||||||
<ul class="flashes">
|
<div class="flashes">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<li>{{message}}</li>
|
<h3>{{message}}</h3>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<main class="container-fluid" style="display:block; margin-top:100px">
|
<main class="container-fluid" style="display:block; height: calc(100vh - 100px); overflow-y: auto;">
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
{% include 'footer.html' %}
|
||||||
|
<style>
|
||||||
|
|
||||||
</script>
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url("{{ url_for('static', filename='images/bg-trees.jpg') }}");
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user