forked from orson/bachemap
started implementing the integrated camera function and the PWA
This commit is contained in:
parent
ea0b967366
commit
e6e6620eb7
28
app.py
28
app.py
@ -205,7 +205,9 @@ def create_app(config=Config):
|
|||||||
else:
|
else:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/manifest.json')
|
||||||
|
def manifest():
|
||||||
|
return send_from_directory('static', 'manifest.json')
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
@login_required
|
@login_required
|
||||||
@ -241,7 +243,31 @@ 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)
|
||||||
|
|
||||||
|
# TODO: Optionally embed latitude/longitude into the photo’s EXIF here
|
||||||
|
# using piexif or Pillow
|
||||||
|
|
||||||
|
# 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')
|
@app.route('/leaderboard')
|
||||||
def leaderboard():
|
def leaderboard():
|
||||||
pipeline = [
|
pipeline = [
|
||||||
|
|||||||
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 |
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -302,7 +302,19 @@ section#pinner-modal::-webkit-scrollbar-thumb {
|
|||||||
.eaflet-popup-content {
|
.eaflet-popup-content {
|
||||||
width: 100%;
|
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 {
|
.marker-cluster {
|
||||||
background-color: rgb(249, 170, 61) !important;
|
background-color: rgb(249, 170, 61) !important;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
<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.">
|
<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.">
|
||||||
|
|
||||||
@ -43,6 +44,8 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@ -58,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<main class="container-fluid" style="padding:0px;height: 100vh; width:100vw; z-index: 5; position: absolute;">
|
<main class="container-fluid" style="padding:0px;height: 100vh; width:100vw; z-index: 5; position: static;">
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -115,5 +118,18 @@ window.addEventListener('load', function() {
|
|||||||
pinnerDesktop.addEventListener('click', toggleSlide);
|
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>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
185
templates/camera.html
Normal file
185
templates/camera.html
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
{% 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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="camera-container">
|
||||||
|
<video id="video" autoplay playsinline></video>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
</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;">Tomar foto</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(180,0,0,0.6);">Nueva foto</button>
|
||||||
|
<button id="send-btn" class="control-button" style="display: none; margin: 10px 10px 10px 5px; flex: 1;">Enviar foto</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">
|
||||||
|
|
||||||
|
<!-- Fallback file input for unsupported browsers -->
|
||||||
|
<div id="file-fallback">
|
||||||
|
<label for="photo">Take/Choose a Photo:</label>
|
||||||
|
<input type="file" name="photo" id="photo" accept="image/*" capture="environment" required>
|
||||||
|
<button type="submit">Upload</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');
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retake photo
|
||||||
|
retakeBtn.addEventListener('click', function() {
|
||||||
|
canvas.style.display = 'none';
|
||||||
|
video.style.display = 'block';
|
||||||
|
captureBtn.style.display = 'inline-block';
|
||||||
|
retakeBtn.style.display = 'none';
|
||||||
|
sendBtn.style.display = 'none';
|
||||||
|
hasImageInput.value = 'false';
|
||||||
|
setupCamera();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send photo
|
||||||
|
sendBtn.addEventListener('click', function() {
|
||||||
|
if (hasImageInput.value === 'true') {
|
||||||
|
// 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
|
||||||
|
formData.append('photo', blob, 'camera-capture.jpg');
|
||||||
|
|
||||||
|
// Submit form data via fetch
|
||||||
|
fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.redirected) {
|
||||||
|
window.location.href = response.url;
|
||||||
|
}
|
||||||
|
}).catch(error => console.error('Error:', error));
|
||||||
|
}, 'image/jpeg', 0.9);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request Geolocation Permission and Set Form Fields
|
||||||
|
if ('geolocation' in navigator) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
document.getElementById('latitude').value = position.coords.latitude;
|
||||||
|
document.getElementById('longitude').value = position.coords.longitude;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error getting location:', error);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 5000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Geolocation not supported in this browser.');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -72,6 +72,7 @@
|
|||||||
// Error handling for geolocation
|
// Error handling for geolocation
|
||||||
function onLocationError(e) {
|
function onLocationError(e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
|
map.setView([20.57, -100.38], 13); // Default to Querétaro's coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Leaflet's built-in location detection
|
// Use Leaflet's built-in location detection
|
||||||
@ -102,4 +103,10 @@
|
|||||||
user_radial.remove();
|
user_radial.remove();
|
||||||
});
|
});
|
||||||
</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"><a href="/camera" class="btn btn-primary">Tomar foto</a></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
<a href="/">
|
<a href="/">
|
||||||
<li style="display: flex; align-items: center;">
|
<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;">
|
<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;"><span style="color: #4a8522;">Bache</span><span style="color: #fa8721;">mapa</span></h3>
|
<h3 style="margin-bottom: 0;" id="sitename"><span style="color: #4a8522;">Bache</span><span style="color: #fa8721;">mapa</span></h3>
|
||||||
</li>
|
</li>
|
||||||
</a>
|
</a>
|
||||||
</ul>
|
</ul>
|
||||||
@ -25,6 +25,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
|
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
|
||||||
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
|
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
|
||||||
|
<li><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>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@ -39,22 +40,27 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
|
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
|
||||||
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
|
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
|
||||||
</ul>
|
|
||||||
</ul>
|
|
||||||
<ul class="desktop-menu">
|
|
||||||
<li><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>
|
<li><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>
|
||||||
</ul>
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media (max-width: 715px) {
|
@media (max-width: 715px) {
|
||||||
.desktop-menu {
|
.desktop-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#sitename {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 716px) {
|
@media (min-width: 716px) {
|
||||||
.mobile-menu {
|
.mobile-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#sitename {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user