Add places layer

This commit is contained in:
Eugene Burmakin 2025-11-17 22:34:38 +01:00
parent 8a36a69987
commit 602975eeaa
13 changed files with 476 additions and 11 deletions

View file

@ -0,0 +1,141 @@
# Places Integration Checklist
## Files Modified:
- ✅ `app/javascript/controllers/stat_page_controller.js` - Added PlacesManager integration
- ✅ `app/javascript/maps/places.js` - Fixed API authentication headers
- ✅ `app/views/stats/_month.html.erb` - Added Places button and tag filters
- ✅ `app/views/shared/_place_creation_modal.html.erb` - Already exists
## What Should Appear:
### On Monthly Stats Page (`/stats/YYYY/MM`):
1. **Map Controls** (top right of map):
- [ ] "Heatmap" button
- [ ] "Points" button
- [ ] **"Places" button** ← NEW!
2. **Below the Map**:
- [ ] **"Filter Places by Tags"** section ← NEW!
- [ ] Checkboxes for each tag you've created
- [ ] Each checkbox shows: icon + name + color dot
## Troubleshooting Steps:
### Step 1: Restart Server
```bash
# Stop server (Ctrl+C)
bundle exec rails server
# Or with Docker:
docker-compose restart web
```
### Step 2: Hard Refresh Browser
- Mac: `Cmd + Shift + R`
- Windows/Linux: `Ctrl + Shift + R`
### Step 3: Check Browser Console
1. Open Developer Tools (F12)
2. Go to Console tab
3. Look for errors (red text)
4. You should see: "StatPage controller connected"
### Step 4: Verify URL
Make sure you're on a monthly stats page:
- ✅ `/stats/2024/11` ← Correct
- ❌ `/stats` ← Wrong (main stats index)
- ❌ `/stats/2024` ← Wrong (yearly stats)
### Step 5: Check JavaScript Loading
In browser console, type:
```javascript
console.log(document.querySelector('[data-controller="stat-page"]'))
```
Should show the element, not null.
### Step 6: Verify Controller Registration
In browser console:
```javascript
console.log(application.controllers)
```
Should include "stat-page" in the list.
## Expected Behavior:
### When You Click "Places" Button:
1. Places layer toggles on/off
2. Button highlights when active
3. Map shows custom markers with tag icons
### When You Check Tag Filters:
1. Map updates immediately
2. Shows only places with selected tags
3. Unchecking all shows all places
## If Nothing Shows:
### Check if you have any places created:
```bash
bundle exec rails console
# In console:
user = User.find_by(email: 'your@email.com')
user.places.count # Should be > 0
user.tags.count # Should be > 0
```
### Create test data:
```bash
bundle exec rails console
user = User.first
tag = user.tags.create!(name: "Test", icon: "📍", color: "#FF5733")
# Create via API or console:
place = user.places.create!(
name: "Test Place",
latitude: 40.7128,
longitude: -74.0060,
source: :manual
)
place.tags << tag
```
## Verification Script:
Run this in Rails console to verify everything:
```ruby
user = User.first
puts "Tags: #{user.tags.count}"
puts "Places: #{user.places.count}"
puts "Places with tags: #{user.places.joins(:tags).distinct.count}"
if user.tags.any?
puts "\nYour tags:"
user.tags.each do |tag|
puts " #{tag.icon} #{tag.name} (#{tag.places.count} places)"
end
end
if user.places.any?
puts "\nYour places:"
user.places.limit(5).each do |place|
puts " #{place.name} at (#{place.latitude}, #{place.longitude})"
puts " Tags: #{place.tags.map(&:name).join(', ')}"
end
end
```
## Still Having Issues?
Check these files exist and have the right content:
- `app/javascript/maps/places.js` - Should export PlacesManager class
- `app/javascript/controllers/stat_page_controller.js` - Should import PlacesManager
- `app/views/stats/_month.html.erb` - Should have Places button at line ~73
Look for JavaScript errors in browser console that might indicate:
- Import/export issues
- Syntax errors
- Missing dependencies

File diff suppressed because one or more lines are too long

View file

@ -37,6 +37,7 @@ import { countryCodesMap } from "../maps/country_codes";
import { VisitsManager } from "../maps/visits";
import { ScratchLayer } from "../maps/scratch_layer";
import { LocationSearch } from "../maps/location_search";
import { PlacesManager } from "../maps/places";
import "leaflet-draw";
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
@ -213,6 +214,10 @@ export default class extends BaseController {
// Expose visits manager globally for location search integration
window.visitsManager = this.visitsManager;
// Initialize the places manager
this.placesManager = new PlacesManager(this.map, this.apiKey);
this.placesManager.initialize();
// Expose maps controller globally for family integration
window.mapsController = this;
@ -258,7 +263,8 @@ export default class extends BaseController {
Areas: this.areasLayer,
Photos: this.photoMarkers,
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer()
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer(),
"Places": this.placesManager.placesLayer
};
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
@ -2216,5 +2222,32 @@ export default class extends BaseController {
});
}
togglePlaceCreationMode() {
if (!this.placesManager) {
console.warn("Places manager not initialized");
return;
}
const button = document.getElementById('create-place-btn');
if (this.placesManager.creationMode) {
// Disable creation mode
this.placesManager.disableCreationMode();
if (button) {
button.classList.remove('btn-error');
button.classList.add('btn-success');
button.title = 'Click to create a place on the map';
}
} else {
// Enable creation mode
this.placesManager.enableCreationMode();
if (button) {
button.classList.remove('btn-success');
button.classList.add('btn-error');
button.title = 'Click map to place marker (click again to cancel)';
}
}
}
}

View file

@ -43,7 +43,7 @@ export default class extends Controller {
try {
const response = await fetch(
`/api/v1/places/nearby?latitude=${latitude}&longitude=${longitude}&limit=5`,
{ headers: { 'APIKEY': this.apiKeyValue } }
{ headers: { 'Authorization': `Bearer ${this.apiKeyValue}` } }
)
if (!response.ok) throw new Error('Failed to load nearby places')
@ -110,7 +110,7 @@ export default class extends Controller {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'APIKEY': this.apiKeyValue
'Authorization': `Bearer ${this.apiKeyValue}`
},
body: JSON.stringify(payload)
})

View file

@ -0,0 +1,41 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
console.log("Places filter controller connected");
}
filterPlaces(event) {
// Get reference to the maps controller's placesManager
const mapsController = window.mapsController;
if (!mapsController || !mapsController.placesManager) {
console.warn("Maps controller or placesManager not found");
return;
}
// Collect all checked tag IDs
const checkboxes = this.element.querySelectorAll('input[type="checkbox"][data-tag-id]');
const selectedTagIds = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => parseInt(cb.dataset.tagId));
console.log("Filtering places by tags:", selectedTagIds);
// Filter places by selected tags (or show all if none selected)
mapsController.placesManager.filterByTags(selectedTagIds.length > 0 ? selectedTagIds : null);
}
clearAll(event) {
event.preventDefault();
// Uncheck all checkboxes
const checkboxes = this.element.querySelectorAll('input[type="checkbox"][data-tag-id]');
checkboxes.forEach(cb => cb.checked = false);
// Show all places
const mapsController = window.mapsController;
if (mapsController && mapsController.placesManager) {
mapsController.placesManager.filterByTags(null);
}
}
}

View file

@ -1,10 +1,11 @@
import L from "leaflet";
import "leaflet.heat";
import { createAllMapLayers } from "../maps/layers";
import { PlacesManager } from "../maps/places";
import BaseController from "./base_controller";
export default class extends BaseController {
static targets = ["map", "loading", "heatmapBtn", "pointsBtn"];
static targets = ["map", "loading", "heatmapBtn", "pointsBtn", "placesBtn"];
connect() {
super.connect();
@ -64,6 +65,10 @@ export default class extends BaseController {
this.markersLayer = L.layerGroup(); // Don't add to map initially
this.heatmapLayer = null;
// Initialize Places Manager
this.placesManager = new PlacesManager(this.map, this.apiKey);
this.placesManager.initialize();
// Load data for this month
this.loadMonthData();
@ -228,6 +233,40 @@ export default class extends BaseController {
}
}
togglePlaces() {
if (!this.placesManager) {
console.warn("Places manager not initialized");
return;
}
if (this.map.hasLayer(this.placesManager.placesLayer)) {
// Remove places layer
this.map.removeLayer(this.placesManager.placesLayer);
if (this.hasPlacesBtnTarget) {
this.placesBtnTarget.classList.remove('btn-active');
}
} else {
// Add places layer
this.map.addLayer(this.placesManager.placesLayer);
if (this.hasPlacesBtnTarget) {
this.placesBtnTarget.classList.add('btn-active');
}
}
}
filterPlacesByTags(event) {
if (!this.placesManager) return;
// Collect all checked tag IDs
const checkboxes = event.currentTarget.closest('[data-controller="stat-page"]').querySelectorAll('input[type="checkbox"][data-tag-id]');
const selectedTagIds = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => parseInt(cb.dataset.tagId));
// Filter places by selected tags (or show all if none selected)
this.placesManager.filterByTags(selectedTagIds.length > 0 ? selectedTagIds : null);
}
showLoading(show) {
if (this.hasLoadingTarget) {
this.loadingTarget.style.display = show ? 'flex' : 'none';

View file

@ -29,7 +29,7 @@ export class PlacesManager {
}
const response = await fetch(url, {
headers: { 'APIKEY': this.apiKey }
headers: { 'Authorization': `Bearer ${this.apiKey}` }
});
if (!response.ok) throw new Error('Failed to load places');
@ -171,7 +171,7 @@ export class PlacesManager {
try {
const response = await fetch(`/api/v1/places/${placeId}`, {
method: 'DELETE',
headers: { 'APIKEY': this.apiKey }
headers: { 'Authorization': `Bearer ${this.apiKey}` }
});
if (!response.ok) throw new Error('Failed to delete place');

View file

@ -98,3 +98,67 @@
</div>
<%= render 'map/settings_modals' %>
<!-- Places Control Buttons -->
<div class="absolute top-4 left-4 z-[1001] flex flex-col gap-2">
<!-- Create Place Button -->
<button id="create-place-btn"
class="btn btn-circle btn-success shadow-lg"
onclick="window.mapsController?.togglePlaceCreationMode()"
title="Click to create a place on the map">
<%= icon 'map-pin-plus' %>
</button>
<!-- Tag Filter Toggle Button -->
<% if current_user.tags.any? %>
<button class="btn btn-circle btn-primary shadow-lg"
onclick="document.getElementById('places-tag-filter').classList.toggle('hidden')"
title="Filter Places by Tags">
<%#= icon 'filter' %>
</button>
<% end %>
</div>
<!-- Tag Filters Panel (Floating) -->
<% if current_user.tags.any? %>
<div id="places-tag-filter" class="absolute top-20 left-4 bg-base-100 rounded-lg shadow-xl p-4 max-w-xs z-[1000] hidden"
data-controller="places-filter">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold flex items-center gap-2">
<%#= icon 'filter' %> Filter Places by Tags
</h3>
<button class="btn btn-ghost btn-xs btn-circle" onclick="document.getElementById('places-tag-filter').classList.add('hidden')">
<%#= icon 'x' %>
</button>
</div>
<div class="space-y-2 max-h-96 overflow-y-auto">
<% current_user.tags.ordered.each do |tag| %>
<label class="flex items-center gap-2 cursor-pointer hover:bg-base-200 p-2 rounded-lg transition-colors">
<input type="checkbox"
data-tag-id="<%= tag.id %>"
data-action="change->places-filter#filterPlaces"
class="checkbox checkbox-sm checkbox-primary">
<span class="text-xl"><%= tag.icon %></span>
<span class="text-sm font-medium flex-1"><%= tag.name %></span>
<% if tag.color.present? %>
<span class="w-3 h-3 rounded-full" style="background-color: <%= tag.color %>;"></span>
<% end %>
</label>
<% end %>
</div>
<div class="mt-3 pt-3 border-t border-base-300">
<button class="btn btn-sm btn-ghost w-full" data-action="click->places-filter#clearAll">
Clear All Filters
</button>
</div>
<div class="mt-2 text-xs text-base-content/70">
<%= icon 'info', class: 'inline w-3 h-3' %> Select tags to filter places. Uncheck all to show all places.
</div>
</div>
<% end %>
<!-- Include Place Creation Modal -->
<%= render 'shared/place_creation_modal' %>

View file

@ -33,6 +33,7 @@
<li><%= link_to 'Visits&nbsp;&amp;&nbsp;Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
<li><%= link_to 'Tags', tags_url, class: "#{active_class?(tags_url)}" %></li>
</ul>
</details>
</li>
@ -99,6 +100,7 @@
<li><%= link_to 'Visits&nbsp;&amp;&nbsp;Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "mx-1 #{active_class?(visits_url)}" %></li>
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
<li><%= link_to 'Tags', tags_url, class: "#{active_class?(tags_url)}" %></li>
</ul>
</details>
</li>

View file

@ -70,6 +70,9 @@
<button class="btn btn-sm btn-outline" data-stat-page-target="pointsBtn" data-action="click->stat-page#togglePoints">
<%= icon 'map-pin' %> Points
</button>
<button class="btn btn-sm btn-outline" data-stat-page-target="placesBtn" data-action="click->stat-page#togglePlaces">
<%= icon 'map-pin-plus' %> Places
</button>
</div>
</div>
@ -83,6 +86,33 @@
</div>
</div>
<!-- Tag Filters -->
<% if current_user.tags.any? %>
<div class="mt-4 p-4 bg-base-200 rounded-lg">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<%= icon 'filter' %> Filter Places by Tags
</h3>
<div class="flex flex-wrap gap-3">
<% current_user.tags.ordered.each do |tag| %>
<label class="flex items-center gap-2 cursor-pointer hover:bg-base-300 px-3 py-2 rounded-lg transition-colors">
<input type="checkbox"
data-tag-id="<%= tag.id %>"
data-action="change->stat-page#filterPlacesByTags"
class="checkbox checkbox-sm checkbox-primary">
<span class="text-xl"><%= tag.icon %></span>
<span class="text-sm font-medium"><%= tag.name %></span>
<% if tag.color.present? %>
<span class="w-3 h-3 rounded-full ml-1" style="background-color: <%= tag.color %>;"></span>
<% end %>
</label>
<% end %>
</div>
<div class="mt-2 text-sm text-base-content/70">
<%= icon 'info' %> Select tags to filter places on the map. Uncheck all to show all places.
</div>
</div>
<% end %>
<!-- Map Stats -->
<!--div class="stats grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div class="stat">
@ -318,3 +348,6 @@
<!-- Include Sharing Modal -->
<%= render 'shared/sharing_modal' %>
<!-- Include Place Creation Modal -->
<%= render 'shared/place_creation_modal' %>

View file

@ -1,6 +1,11 @@
class AddUserIdToPlaces < ActiveRecord::Migration[8.0]
def change
disable_ddl_transaction!
def up
# Add nullable for backward compatibility, will enforce later via data migration
add_reference :places, :user, null: true, foreign_key: true, index: true
add_reference :places, :user, null: true, index: {algorithm: :concurrently} unless foreign_key_exists?(:places, :users)
end
def down
remove_reference :places, :user, index: true if foreign_key_exists?(:places, :users)
end
end

4
db/schema.rb generated
View file

@ -316,6 +316,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_16_134520) do
t.datetime "updated_at", null: false
t.geometry "path", limit: {srid: 3857, type: "line_string"}
t.jsonb "visited_countries", default: {}, null: false
t.uuid "sharing_uuid"
t.jsonb "sharing_settings", default: {}
t.index ["sharing_uuid"], name: "index_trips_on_sharing_uuid", unique: true
t.index ["user_id"], name: "index_trips_on_user_id"
end
@ -383,7 +386,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_16_134520) do
add_foreign_key "notifications", "users"
add_foreign_key "place_visits", "places"
add_foreign_key "place_visits", "visits"
add_foreign_key "places", "users"
add_foreign_key "points", "users"
add_foreign_key "points", "visits"
add_foreign_key "stats", "users"

105
verify_places_integration.rb Executable file
View file

@ -0,0 +1,105 @@
# Run with: bundle exec rails runner verify_places_integration.rb
puts "🔍 Verifying Places Integration..."
puts "=" * 50
# Check files exist
files_to_check = [
'app/javascript/maps/places.js',
'app/javascript/controllers/stat_page_controller.js',
'app/javascript/controllers/place_creation_controller.js',
'app/views/stats/_month.html.erb',
'app/views/shared/_place_creation_modal.html.erb'
]
puts "\n📁 Checking Files:"
files_to_check.each do |file|
if File.exist?(file)
puts "#{file}"
else
puts " ❌ MISSING: #{file}"
end
end
# Check view has our changes
puts "\n🎨 Checking View Changes:"
month_view = File.read('app/views/stats/_month.html.erb')
if month_view.include?('placesBtn')
puts " ✅ Places button found in view"
else
puts " ❌ Places button NOT found in view"
end
if month_view.include?('Filter Places by Tags')
puts " ✅ Tag filter section found in view"
else
puts " ❌ Tag filter section NOT found in view"
end
if month_view.include?('place_creation_modal')
puts " ✅ Place creation modal included"
else
puts " ❌ Place creation modal NOT included"
end
# Check JavaScript has our changes
puts "\n💻 Checking JavaScript Changes:"
controller_js = File.read('app/javascript/controllers/stat_page_controller.js')
if controller_js.include?('PlacesManager')
puts " ✅ PlacesManager imported"
else
puts " ❌ PlacesManager NOT imported"
end
if controller_js.include?('togglePlaces()')
puts " ✅ togglePlaces() method found"
else
puts " ❌ togglePlaces() method NOT found"
end
if controller_js.include?('filterPlacesByTags')
puts " ✅ filterPlacesByTags() method found"
else
puts " ❌ filterPlacesByTags() method NOT found"
end
# Check database
puts "\n🗄️ Checking Database:"
user = User.first
if user
puts " ✅ Found user: #{user.email}"
puts " Tags: #{user.tags.count}"
puts " Places: #{user.places.count}"
if user.tags.any?
puts "\n 📌 Your Tags:"
user.tags.limit(5).each do |tag|
puts " #{tag.icon} #{tag.name} (#{tag.places.count} places)"
end
else
puts " ⚠️ No tags created yet. Create some at /tags"
end
if user.places.any?
puts "\n 📍 Your Places:"
user.places.limit(5).each do |place|
puts " #{place.name} - #{place.tags.map(&:name).join(', ')}"
end
else
puts " ⚠️ No places created yet. Use the API or create via console."
end
else
puts " ❌ No users found"
end
puts "\n" + "=" * 50
puts "✅ Integration files are in place!"
puts "\n📋 Next Steps:"
puts " 1. Restart your Rails server"
puts " 2. Hard refresh your browser (Cmd+Shift+R)"
puts " 3. Navigate to /stats/#{Date.today.year}/#{Date.today.month}"
puts " 4. Look for 'Places' button next to 'Heatmap' and 'Points'"
puts " 5. Create tags at /tags if you haven't already"
puts " 6. Create places via API with those tags"