const app = () => ({ futureTimes: [], 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", async init() { if (this.selectedLocation) { this.searchQuery = this.formatLocationLabel(this.selectedLocation); await this.refreshPrayerTimes(); } setInterval(() => { this.now = new Date(); }, 500); 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"; }, 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() { if (!this.debug) { return this.now; } const d = new Date(); d.setHours(0, this.userMinutes, 0, 0); return d; }, get userTime() { return formatTime(this.userNow); }, get todayTimes() { if (this.futureTimes.length === 0) { return null; } return new PrayerTimes(this.futureTimes[0], () => this.userNow); } }); class PrayerTimes { static salaths = ["fajr", "sunrise", "dhuhr", "asr", "maghrib", "isha"]; static translations = { 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; } get times() { 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] ?? "??", startsAt, timeLocal: this.salathTimes[k], get untilSeconds() { const untilSeconds = (startsAt - now) / 1000; return now > startsAt ? 0 : untilSeconds; }, get untilHuman() { return formatDuration(this.untilSeconds); } }; }); } get currentSalath() { let current = this.times.filter((it) => it.untilSeconds === 0).at(-1); if (current === undefined) { 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; } get nextSalath() { let next = this.times.filter((it) => it.untilSeconds > 0)[0]; if (next === undefined) { const nextDay = new Date(this.date); nextDay.setDate(nextDay.getDate() + 1); next = new PrayerTimes({date: nextDay.toISOString(), ...this.salathTimes}, this.clock).times[0]; } return { ...next }; } } function formatDuration(seconds) { const d = new Date(0, 0, 0, 0, 0, seconds); return formatTime(d); } function formatTime(then) { return new Intl.DateTimeFormat(navigator.language, { hour: "numeric", minute: "numeric", second: "numeric" }).format(then); } function formatDate(then) { return new Intl.DateTimeFormat(navigator.language, { year: "numeric", month: "2-digit", day: "2-digit" }).format(then); } async function fetchJSON(url, req = {}) { const res = await fetch(url, { ...req }); if (!res.ok) { throw new Error(`request failed with status ${res.status}`); } return res.json(); } function getUserLocation() { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error("Geolocation is not supported by this browser.")); return; } navigator.geolocation.getCurrentPosition( (position) => resolve(position.coords), (error) => reject(error) ); }); }