mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Add places layer
This commit is contained in:
parent
8a36a69987
commit
602975eeaa
13 changed files with 476 additions and 11 deletions
141
PLACES_INTEGRATION_CHECKLIST.md
Normal file
141
PLACES_INTEGRATION_CHECKLIST.md
Normal 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
|
|
@ -37,6 +37,7 @@ import { countryCodesMap } from "../maps/country_codes";
|
||||||
import { VisitsManager } from "../maps/visits";
|
import { VisitsManager } from "../maps/visits";
|
||||||
import { ScratchLayer } from "../maps/scratch_layer";
|
import { ScratchLayer } from "../maps/scratch_layer";
|
||||||
import { LocationSearch } from "../maps/location_search";
|
import { LocationSearch } from "../maps/location_search";
|
||||||
|
import { PlacesManager } from "../maps/places";
|
||||||
|
|
||||||
import "leaflet-draw";
|
import "leaflet-draw";
|
||||||
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
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
|
// Expose visits manager globally for location search integration
|
||||||
window.visitsManager = this.visitsManager;
|
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
|
// Expose maps controller globally for family integration
|
||||||
window.mapsController = this;
|
window.mapsController = this;
|
||||||
|
|
||||||
|
|
@ -258,7 +263,8 @@ export default class extends BaseController {
|
||||||
Areas: this.areasLayer,
|
Areas: this.areasLayer,
|
||||||
Photos: this.photoMarkers,
|
Photos: this.photoMarkers,
|
||||||
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
|
"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);
|
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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default class extends Controller {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/v1/places/nearby?latitude=${latitude}&longitude=${longitude}&limit=5`,
|
`/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')
|
if (!response.ok) throw new Error('Failed to load nearby places')
|
||||||
|
|
@ -110,7 +110,7 @@ export default class extends Controller {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'APIKEY': this.apiKeyValue
|
'Authorization': `Bearer ${this.apiKeyValue}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
41
app/javascript/controllers/places_filter_controller.js
Normal file
41
app/javascript/controllers/places_filter_controller.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import "leaflet.heat";
|
import "leaflet.heat";
|
||||||
import { createAllMapLayers } from "../maps/layers";
|
import { createAllMapLayers } from "../maps/layers";
|
||||||
|
import { PlacesManager } from "../maps/places";
|
||||||
import BaseController from "./base_controller";
|
import BaseController from "./base_controller";
|
||||||
|
|
||||||
export default class extends BaseController {
|
export default class extends BaseController {
|
||||||
static targets = ["map", "loading", "heatmapBtn", "pointsBtn"];
|
static targets = ["map", "loading", "heatmapBtn", "pointsBtn", "placesBtn"];
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
super.connect();
|
super.connect();
|
||||||
|
|
@ -64,6 +65,10 @@ export default class extends BaseController {
|
||||||
this.markersLayer = L.layerGroup(); // Don't add to map initially
|
this.markersLayer = L.layerGroup(); // Don't add to map initially
|
||||||
this.heatmapLayer = null;
|
this.heatmapLayer = null;
|
||||||
|
|
||||||
|
// Initialize Places Manager
|
||||||
|
this.placesManager = new PlacesManager(this.map, this.apiKey);
|
||||||
|
this.placesManager.initialize();
|
||||||
|
|
||||||
// Load data for this month
|
// Load data for this month
|
||||||
this.loadMonthData();
|
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) {
|
showLoading(show) {
|
||||||
if (this.hasLoadingTarget) {
|
if (this.hasLoadingTarget) {
|
||||||
this.loadingTarget.style.display = show ? 'flex' : 'none';
|
this.loadingTarget.style.display = show ? 'flex' : 'none';
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export class PlacesManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: { 'APIKEY': this.apiKey }
|
headers: { 'Authorization': `Bearer ${this.apiKey}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to load places');
|
if (!response.ok) throw new Error('Failed to load places');
|
||||||
|
|
@ -171,7 +171,7 @@ export class PlacesManager {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/places/${placeId}`, {
|
const response = await fetch(`/api/v1/places/${placeId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'APIKEY': this.apiKey }
|
headers: { 'Authorization': `Bearer ${this.apiKey}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to delete place');
|
if (!response.ok) throw new Error('Failed to delete place');
|
||||||
|
|
|
||||||
|
|
@ -98,3 +98,67 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render 'map/settings_modals' %>
|
<%= 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' %>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
<li><%= link_to 'Visits & Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
|
<li><%= link_to 'Visits & 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 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||||
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_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>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -99,6 +100,7 @@
|
||||||
<li><%= link_to 'Visits & Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "mx-1 #{active_class?(visits_url)}" %></li>
|
<li><%= link_to 'Visits & 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 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||||
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_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>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@
|
||||||
<button class="btn btn-sm btn-outline" data-stat-page-target="pointsBtn" data-action="click->stat-page#togglePoints">
|
<button class="btn btn-sm btn-outline" data-stat-page-target="pointsBtn" data-action="click->stat-page#togglePoints">
|
||||||
<%= icon 'map-pin' %> Points
|
<%= icon 'map-pin' %> Points
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -83,6 +86,33 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Map Stats -->
|
||||||
<!--div class="stats grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
<!--div class="stats grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
|
|
@ -318,3 +348,6 @@
|
||||||
|
|
||||||
<!-- Include Sharing Modal -->
|
<!-- Include Sharing Modal -->
|
||||||
<%= render 'shared/sharing_modal' %>
|
<%= render 'shared/sharing_modal' %>
|
||||||
|
|
||||||
|
<!-- Include Place Creation Modal -->
|
||||||
|
<%= render 'shared/place_creation_modal' %>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
class AddUserIdToPlaces < ActiveRecord::Migration[8.0]
|
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 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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
4
db/schema.rb
generated
4
db/schema.rb
generated
|
|
@ -316,6 +316,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_16_134520) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.geometry "path", limit: {srid: 3857, type: "line_string"}
|
t.geometry "path", limit: {srid: 3857, type: "line_string"}
|
||||||
t.jsonb "visited_countries", default: {}, null: false
|
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"
|
t.index ["user_id"], name: "index_trips_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -383,7 +386,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_16_134520) do
|
||||||
add_foreign_key "notifications", "users"
|
add_foreign_key "notifications", "users"
|
||||||
add_foreign_key "place_visits", "places"
|
add_foreign_key "place_visits", "places"
|
||||||
add_foreign_key "place_visits", "visits"
|
add_foreign_key "place_visits", "visits"
|
||||||
add_foreign_key "places", "users"
|
|
||||||
add_foreign_key "points", "users"
|
add_foreign_key "points", "users"
|
||||||
add_foreign_key "points", "visits"
|
add_foreign_key "points", "visits"
|
||||||
add_foreign_key "stats", "users"
|
add_foreign_key "stats", "users"
|
||||||
|
|
|
||||||
105
verify_places_integration.rb
Executable file
105
verify_places_integration.rb
Executable 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"
|
||||||
Loading…
Reference in a new issue