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