mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Update place creating modal
This commit is contained in:
parent
64d299b363
commit
e1013b1ae1
6 changed files with 128 additions and 43 deletions
File diff suppressed because one or more lines are too long
|
|
@ -2,13 +2,16 @@ import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["modal", "form", "nameInput", "latitudeInput", "longitudeInput",
|
static targets = ["modal", "form", "nameInput", "latitudeInput", "longitudeInput",
|
||||||
"nearbyList", "loadingSpinner", "tagCheckboxes"]
|
"nearbyList", "loadingSpinner", "tagCheckboxes", "loadMoreContainer", "loadMoreButton"]
|
||||||
static values = {
|
static values = {
|
||||||
apiKey: String
|
apiKey: String
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.setupEventListeners()
|
this.setupEventListeners()
|
||||||
|
this.currentRadius = 0.5 // Start with 500m (0.5km)
|
||||||
|
this.maxRadius = 1.5 // Max 1500m (1.5km)
|
||||||
|
this.setupTagListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
|
|
@ -17,9 +20,36 @@ export default class extends Controller {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupTagListeners() {
|
||||||
|
// Listen for checkbox changes to update badge styling
|
||||||
|
if (this.hasTagCheckboxesTarget) {
|
||||||
|
this.tagCheckboxesTarget.addEventListener('change', (e) => {
|
||||||
|
if (e.target.type === 'checkbox' && e.target.name === 'tag_ids[]') {
|
||||||
|
const badge = e.target.nextElementSibling
|
||||||
|
const color = badge.dataset.color
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
|
// Filled style
|
||||||
|
badge.classList.remove('badge-outline')
|
||||||
|
badge.style.backgroundColor = color
|
||||||
|
badge.style.borderColor = color
|
||||||
|
badge.style.color = 'white'
|
||||||
|
} else {
|
||||||
|
// Outline style
|
||||||
|
badge.classList.add('badge-outline')
|
||||||
|
badge.style.backgroundColor = 'transparent'
|
||||||
|
badge.style.borderColor = color
|
||||||
|
badge.style.color = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async open(latitude, longitude) {
|
async open(latitude, longitude) {
|
||||||
this.latitudeInputTarget.value = latitude
|
this.latitudeInputTarget.value = latitude
|
||||||
this.longitudeInputTarget.value = longitude
|
this.longitudeInputTarget.value = longitude
|
||||||
|
this.currentRadius = 0.5 // Reset radius when opening modal
|
||||||
|
|
||||||
this.modalTarget.classList.add('modal-open')
|
this.modalTarget.classList.add('modal-open')
|
||||||
this.nameInputTarget.focus()
|
this.nameInputTarget.focus()
|
||||||
|
|
@ -31,25 +61,43 @@ export default class extends Controller {
|
||||||
this.modalTarget.classList.remove('modal-open')
|
this.modalTarget.classList.remove('modal-open')
|
||||||
this.formTarget.reset()
|
this.formTarget.reset()
|
||||||
this.nearbyListTarget.innerHTML = ''
|
this.nearbyListTarget.innerHTML = ''
|
||||||
|
this.loadMoreContainerTarget.classList.add('hidden')
|
||||||
|
this.currentRadius = 0.5
|
||||||
|
|
||||||
const event = new CustomEvent('place:create:cancelled')
|
const event = new CustomEvent('place:create:cancelled')
|
||||||
document.dispatchEvent(event)
|
document.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadNearbyPlaces(latitude, longitude) {
|
async loadNearbyPlaces(latitude, longitude, radius = null) {
|
||||||
this.loadingSpinnerTarget.classList.remove('hidden')
|
this.loadingSpinnerTarget.classList.remove('hidden')
|
||||||
this.nearbyListTarget.innerHTML = ''
|
|
||||||
|
// Use provided radius or current radius
|
||||||
|
const searchRadius = radius || this.currentRadius
|
||||||
|
const isLoadingMore = radius !== null && radius > this.currentRadius - 0.5
|
||||||
|
|
||||||
|
// Only clear the list on initial load, not when loading more
|
||||||
|
if (!isLoadingMore) {
|
||||||
|
this.nearbyListTarget.innerHTML = ''
|
||||||
|
}
|
||||||
|
|
||||||
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}&radius=${searchRadius}&limit=5`,
|
||||||
{ headers: { 'Authorization': `Bearer ${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')
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
this.renderNearbyPlaces(data.places)
|
this.renderNearbyPlaces(data.places, isLoadingMore)
|
||||||
|
|
||||||
|
// Show load more button if we can expand radius further
|
||||||
|
if (searchRadius < this.maxRadius) {
|
||||||
|
this.loadMoreContainerTarget.classList.remove('hidden')
|
||||||
|
this.updateLoadMoreButton(searchRadius)
|
||||||
|
} else {
|
||||||
|
this.loadMoreContainerTarget.classList.add('hidden')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading nearby places:', error)
|
console.error('Error loading nearby places:', error)
|
||||||
this.nearbyListTarget.innerHTML = '<p class="text-error">Failed to load suggestions</p>'
|
this.nearbyListTarget.innerHTML = '<p class="text-error">Failed to load suggestions</p>'
|
||||||
|
|
@ -58,27 +106,59 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNearbyPlaces(places) {
|
renderNearbyPlaces(places, append = false) {
|
||||||
if (!places || places.length === 0) {
|
if (!places || places.length === 0) {
|
||||||
this.nearbyListTarget.innerHTML = '<p class="text-sm text-gray-500">No nearby places found</p>'
|
if (!append) {
|
||||||
|
this.nearbyListTarget.innerHTML = '<p class="text-sm text-gray-500">No nearby places found</p>'
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = places.map(place => `
|
// Calculate starting index based on existing items
|
||||||
|
const currentCount = append ? this.nearbyListTarget.querySelectorAll('.card').length : 0
|
||||||
|
|
||||||
|
const html = places.map((place, index) => `
|
||||||
<div class="card card-compact bg-base-200 cursor-pointer hover:bg-base-300 transition"
|
<div class="card card-compact bg-base-200 cursor-pointer hover:bg-base-300 transition"
|
||||||
data-action="click->place-creation#selectNearby"
|
data-action="click->place-creation#selectNearby"
|
||||||
data-place-name="${this.escapeHtml(place.name)}"
|
data-place-name="${this.escapeHtml(place.name)}"
|
||||||
data-place-latitude="${place.latitude}"
|
data-place-latitude="${place.latitude}"
|
||||||
data-place-longitude="${place.longitude}">
|
data-place-longitude="${place.longitude}">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="font-semibold">${this.escapeHtml(place.name)}</h4>
|
<div class="flex gap-2">
|
||||||
${place.street ? `<p class="text-sm">${this.escapeHtml(place.street)}</p>` : ''}
|
<span class="badge badge-primary badge-sm">#${currentCount + index + 1}</span>
|
||||||
${place.city ? `<p class="text-xs text-gray-500">${this.escapeHtml(place.city)}, ${this.escapeHtml(place.country || '')}</p>` : ''}
|
<div class="flex-1">
|
||||||
|
<h4 class="font-semibold">${this.escapeHtml(place.name)}</h4>
|
||||||
|
${place.street ? `<p class="text-sm">${this.escapeHtml(place.street)}</p>` : ''}
|
||||||
|
${place.city ? `<p class="text-xs text-gray-500">${this.escapeHtml(place.city)}, ${this.escapeHtml(place.country || '')}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')
|
`).join('')
|
||||||
|
|
||||||
this.nearbyListTarget.innerHTML = html
|
if (append) {
|
||||||
|
this.nearbyListTarget.insertAdjacentHTML('beforeend', html)
|
||||||
|
} else {
|
||||||
|
this.nearbyListTarget.innerHTML = html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMore() {
|
||||||
|
// Increase radius by 500m (0.5km) up to max of 1500m (1.5km)
|
||||||
|
if (this.currentRadius >= this.maxRadius) return
|
||||||
|
|
||||||
|
this.currentRadius = Math.min(this.currentRadius + 0.5, this.maxRadius)
|
||||||
|
|
||||||
|
const latitude = parseFloat(this.latitudeInputTarget.value)
|
||||||
|
const longitude = parseFloat(this.longitudeInputTarget.value)
|
||||||
|
|
||||||
|
await this.loadNearbyPlaces(latitude, longitude, this.currentRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLoadMoreButton(currentRadius) {
|
||||||
|
const nextRadius = Math.min(currentRadius + 0.5, this.maxRadius)
|
||||||
|
const radiusInMeters = Math.round(nextRadius * 1000)
|
||||||
|
this.loadMoreButtonTarget.textContent = `Load More (search up to ${radiusInMeters}m)`
|
||||||
}
|
}
|
||||||
|
|
||||||
selectNearby(event) {
|
selectNearby(event) {
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,6 @@
|
||||||
<input type="hidden" name="longitude" data-place-creation-target="longitudeInput">
|
<input type="hidden" name="longitude" data-place-creation-target="longitudeInput">
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Nearby Places Suggestions</span>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="loading loading-spinner loading-sm absolute top-2 right-2 hidden" data-place-creation-target="loadingSpinner"></div>
|
|
||||||
<div class="space-y-2 max-h-48 overflow-y-auto" data-place-creation-target="nearbyList">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider">OR</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text font-semibold">Place Name *</span>
|
<span class="label-text font-semibold">Place Name *</span>
|
||||||
|
|
@ -57,7 +44,7 @@
|
||||||
<% current_user.tags.ordered.each do |tag| %>
|
<% current_user.tags.ordered.each do |tag| %>
|
||||||
<label class="cursor-pointer">
|
<label class="cursor-pointer">
|
||||||
<input type="checkbox" name="tag_ids[]" value="<%= tag.id %>" class="checkbox checkbox-sm hidden peer">
|
<input type="checkbox" name="tag_ids[]" value="<%= tag.id %>" class="checkbox checkbox-sm hidden peer">
|
||||||
<span class="badge badge-lg peer-checked:badge-primary" style="background-color: <%= tag.color %>">
|
<span class="badge badge-lg badge-outline transition-all peer-checked:scale-105" style="border-color: <%= tag.color %>; color: <%= tag.color %>;" data-color="<%= tag.color %>">
|
||||||
<%= tag.icon %> #<%= tag.name %>
|
<%= tag.icon %> #<%= tag.name %>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -67,6 +54,28 @@
|
||||||
<span class="label-text-alt">Click tags to select them for this place</span>
|
<span class="label-text-alt">Click tags to select them for this place</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">Suggested Places</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">Nearby Places</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="loading loading-spinner loading-sm absolute top-2 right-2 hidden" data-place-creation-target="loadingSpinner"></div>
|
||||||
|
<div class="space-y-2 max-h-48 overflow-y-auto" data-place-creation-target="nearbyList">
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-center hidden" data-place-creation-target="loadMoreContainer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
data-action="click->place-creation#loadMore"
|
||||||
|
data-place-creation-target="loadMoreButton">
|
||||||
|
Load More (expand search radius)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex gap-2 justify-end">
|
||||||
<%= link_to "Edit", edit_tag_path(tag), class: "btn btn-sm btn-ghost" %>
|
<%= link_to "Edit", edit_tag_path(tag), class: "btn btn-sm btn-ghost" %>
|
||||||
<%= button_to "Delete", tag_path(tag), method: :delete,
|
<%= button_to "Delete", tag_path(tag), method: :delete,
|
||||||
data: { turbo_confirm: "Are you sure?" },
|
data: { turbo_confirm: "Are you sure?", turbo_method: :delete },
|
||||||
class: "btn btn-sm btn-error" %>
|
class: "btn btn-sm btn-error" %>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
6
db/schema.rb
generated
6
db/schema.rb
generated
|
|
@ -319,9 +319,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_18_210506) 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
|
||||||
|
|
||||||
|
|
@ -347,9 +344,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_18_210506) do
|
||||||
t.integer "points_count", default: 0, null: false
|
t.integer "points_count", default: 0, null: false
|
||||||
t.string "provider"
|
t.string "provider"
|
||||||
t.string "uid"
|
t.string "uid"
|
||||||
t.text "patreon_access_token"
|
|
||||||
t.text "patreon_refresh_token"
|
|
||||||
t.datetime "patreon_token_expires_at"
|
|
||||||
t.string "utm_source"
|
t.string "utm_source"
|
||||||
t.string "utm_medium"
|
t.string "utm_medium"
|
||||||
t.string "utm_campaign"
|
t.string "utm_campaign"
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ if Tag.none?
|
||||||
{ name: 'Travel Plans', color: '#F1C40F', icon: '🗺️' },
|
{ name: 'Travel Plans', color: '#F1C40F', icon: '🗺️' },
|
||||||
]
|
]
|
||||||
|
|
||||||
default_tags.each do |tag_attrs|
|
User.find_each do |user|
|
||||||
Tag.create!(tag_attrs)
|
default_tags.each do |tag_attrs|
|
||||||
|
Tag.create!(tag_attrs.merge(user: user))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue