feat: Add location search

This commit is contained in:
abdus committed 2026-02-21 01:50:59 +03:00
1 parent 26af8af8a9
commit a53b200c21
5 files changed
+327 -105

No files matched your search

+132 -96
View File
@@ -1,29 +1,115 @@
const app = () => ({
futureTimes: [],
locationId: Alpine.$persist('11002'),
selectedLocation: Alpine.$persist(null),
lastUpdated: Alpine.$persist(null),
searchQuery: "",
searchResults: [],
searchOpen: false,
searchLoading: false,
searchError: "",
searchDebounceTimer: null,
userMinutes: 0,
now: new Date(),
debug: location.hash === '#debug',
geolocation: null,
debug: location.hash === "#debug",
async init() {
await this.refreshIfStale();
if (this.selectedLocation) {
this.searchQuery = this.formatLocationLabel(this.selectedLocation);
await this.refreshPrayerTimes();
}
setInterval(() => {
this.now = new Date();
}, 500);
getUserLocation()
.then(loc => {
this.geolocation = {latitude: loc.latitude, longitude: loc.longitude};
this.refreshIfStale();
})
.catch(() => this.geolocation = null)
try {
const coords = await getUserLocation();
await this.selectNearestLocation(coords.latitude, coords.longitude);
} catch (_error) {
// Ignore geolocation errors and rely on manual search.
}
},
onHash() {
this.debug = location.hash === '#debug'
this.debug = location.hash === "#debug";
},
onSearchInput() {
this.searchError = "";
if (this.searchDebounceTimer) {
clearTimeout(this.searchDebounceTimer);
}
const query = this.searchQuery.trim();
if (query === "") {
this.searchResults = [];
this.searchOpen = false;
return;
}
this.searchDebounceTimer = setTimeout(() => {
this.searchLocations(query);
}, 250);
},
async searchLocations(query) {
this.searchLoading = true;
this.searchOpen = true;
try {
const response = await fetchJSON(`/api/v1/diyanet/location?query=${encodeURIComponent(query)}`);
this.searchResults = response.locations ?? [];
} catch (_error) {
this.searchResults = [];
this.searchError = "Failed to search locations.";
} finally {
this.searchLoading = false;
}
},
async selectNearestLocation(latitude, longitude) {
const response = await fetchJSON(`/api/v1/diyanet/location?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}`);
const first = (response.locations ?? [])[0];
if (!first) {
return;
}
await this.selectLocation(first);
},
async selectLocation(location) {
this.selectedLocation = location;
this.searchQuery = this.formatLocationLabel(location);
this.searchOpen = false;
this.searchResults = [];
await this.refreshPrayerTimes();
},
formatLocationLabel(location) {
if (!location) {
return "";
}
const country = (location.country_code || "").trim();
const name = (location.name || "").trim();
const asciiName = (location.ascii_name || "").trim();
if (name && asciiName && name !== asciiName) {
return `${name} (${asciiName})${country ? ` - ${country}` : ""}`;
}
if (name) {
return `${name}${country ? ` - ${country}` : ""}`;
}
return `${asciiName}${country ? ` - ${country}` : ""}`;
},
async refreshPrayerTimes() {
if (!this.selectedLocation) {
this.futureTimes = [];
return;
}
const latitude = this.selectedLocation.latitude;
const longitude = this.selectedLocation.longitude;
const response = await fetchJSON(`/api/v1/diyanet/prayertimes?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}`);
this.futureTimes = response.prayertimes ?? [];
this.lastUpdated = new Date().toISOString();
},
get userNow() {
@@ -40,90 +126,66 @@ const app = () => ({
return formatTime(this.userNow);
},
async refreshIfStale() {
const updatedAt = new Date(this.lastUpdated);
const now = new Date();
const elapsedSeconds = (now - updatedAt) / 1000;
if (this.geolocation !== null) {
const response = await fetchJSON(`/api/v1/diyanet/prayertimes?latitude=${this.geolocation.latitude}&longitude=${this.geolocation.longitude}`);
this.futureTimes = response.prayertimes ?? [];
} else {
const response = await fetchJSON(`/api/v1/diyanet/prayertimes?location_id=${this.locationId}`);
this.futureTimes = response.prayertimes ?? [];
}
this.lastUpdated = now.toISOString();
},
get todayTimes() {
if (this.futureTimes.length === 0) {
return null;
}
return new PrayerTimes(this.futureTimes[0], () => this.userNow);
},
translate(key, lang) {
return translations[key][lang] ?? key
}
});
class PrayerTimes {
static salaths = ['fajr', 'sunrise', 'dhuhr', 'asr', 'maghrib', 'isha'];
static salaths = ["fajr", "sunrise", "dhuhr", "asr", "maghrib", "isha"];
static translations = {
fajr: {tr: 'İmsak', de: 'Frühgebet', ar: 'صلاة الفجر'},
sunrise: {tr: 'Güneş', de: 'Sonnenaufgang', ar: 'الشروق'},
dhuhr: {tr: 'Öğle', de: 'Mittagsgebet', ar: 'صلاة الظهر'},
asr: {tr: 'İkindi', de: 'Nachmittagsgebet', ar: 'صلاة العصر'},
maghrib: {tr: 'Akşam', de: 'Abendgebet', ar: 'صلاة المغرب'},
isha: {tr: 'Yatsı', de: 'Nachtgebet', ar: 'صلاة العشاء'},
}
fajr: {tr: "İmsak", de: "Fruehgebet", ar: "صلاة الفجر"},
sunrise: {tr: "Günes", de: "Sonnenaufgang", ar: "الشروق"},
dhuhr: {tr: "Öğle", de: "Mittagsgebet", ar: "صلاة الظهر"},
asr: {tr: "İkindi", de: "Nachmittagsgebet", ar: "صلاة العصر"},
maghrib: {tr: "Aksam", de: "Abendgebet", ar: "صلاة المغرب"},
isha: {tr: "Yatsı", de: "Nachtgebet", ar: "صلاة العشاء"}
};
constructor({date, ...rest}, clock = () => new Date()) {
this.date = date;
this.clock = clock
this.salathTimes = rest
this.clock = clock;
this.salathTimes = rest;
}
get times() {
const now = this.clock()
return PrayerTimes.salaths.map(k => {
// "2023-03-05T00:00:00Z"
const startsAt = new Date(this.date.replace('T00:00', `T${this.salathTimes[k]}`).replace(/Z$/, ''));
const now = this.clock();
return PrayerTimes.salaths.map((k) => {
const startsAt = new Date(this.date.replace("T00:00", `T${this.salathTimes[k]}`).replace(/Z$/, ""));
return {
salath: k,
name: lang => PrayerTimes.translations[k][lang] ?? '??',
name: (lang) => PrayerTimes.translations[k][lang] ?? "??",
startsAt,
timeLocal: this.salathTimes[k],
get untilSeconds() {
let untilSeconds = (startsAt - now) / 1000;
const untilSeconds = (startsAt - now) / 1000;
return now > startsAt ? 0 : untilSeconds;
},
get untilHuman() {
return formatDuration(this.untilSeconds)
},
}
})
return formatDuration(this.untilSeconds);
}
};
});
}
get currentSalath() {
let current = this.times.filter(it => it.untilSeconds === 0).at(-1);
let current = this.times.filter((it) => it.untilSeconds === 0).at(-1);
if (current === undefined) {
// we're in isha -> today's fajr is almost the same as tomorrows
const prevDay = new Date(this.date);
prevDay.setDate(prevDay.getDate() - 1);
current = new PrayerTimes({date: prevDay.toISOString(), ...this.salathTimes}, this.clock).times.at(-1);
}
return current
return current;
}
get nextSalath() {
let next = this.times
.filter(it => it.untilSeconds > 0)[0]
let next = this.times.filter((it) => it.untilSeconds > 0)[0];
if (next === undefined) {
// we're in isha -> today's fajr is almost the same as tomorrows
const nextDay = new Date(this.date);
nextDay.setDate(nextDay.getDate() + 1);
@@ -131,24 +193,16 @@ class PrayerTimes {
}
return {
...next,
}
...next
};
}
}
/**
* @param {number} seconds
* @return {string}
* */
function formatDuration(seconds) {
const d = new Date(0, 0, 0, 0, 0, seconds);
return formatTime(d);
}
/**
* @param {Date} then
* @return {string}
* */
function formatTime(then) {
return new Intl.DateTimeFormat(navigator.language, {
hour: "numeric",
@@ -157,10 +211,6 @@ function formatTime(then) {
}).format(then);
}
/**
* @param {Date} then
* @return {string}
* */
function formatDate(then) {
return new Intl.DateTimeFormat(navigator.language, {
year: "numeric",
@@ -169,42 +219,28 @@ function formatDate(then) {
}).format(then);
}
/**
* @param {Date} then
* @return {string}
* */
function formatDateHijri(then) {
return new Intl.DateTimeFormat("en-u-ca-islamic-umalqura-nu-latn", {
year: "numeric",
month: "long",
day: "numeric",
}).format(then);
}
/**
* @param {string} url
* @param {RequestInit} req
* */
async function fetchJSON(url, req = {}) {
const res = await fetch(url, {
...req,
})
return res.json()
...req
});
if (!res.ok) {
throw new Error(`request failed with status ${res.status}`);
}
return res.json();
}
/**
* @return {Promise<GeolocationCoordinates>}
* */
function getUserLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject("Geolocation is not supported by this browser.");
reject(new Error("Geolocation is not supported by this browser."));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position.coords),
(error) => reject(error.message)
(error) => reject(error)
);
});
}