You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

311 lines
8.9 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

const app = () => ({
futureTimes: [],
selectedLocation: Alpine.$persist(null),
lastUpdated: Alpine.$persist(null),
searchQuery: "",
searchResults: [],
searchOpen: false,
searchLoading: false,
searchError: "",
searchDebounceTimer: null,
supportedSalathLangs: ["tr", "de", "en", "ar"],
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);
},
get preferredSalathLangs() {
const browserLanguages = (navigator.languages && navigator.languages.length > 0)
? navigator.languages
: [navigator.language];
const preferred = browserLanguages
.filter(Boolean)
.map((tag) => normalizeLanguageTag(tag))
.filter((lang) => this.supportedSalathLangs.includes(lang));
return Array.from(new Set([...preferred, ...this.supportedSalathLangs]));
},
salathNameDisplay(salath) {
const names = this.preferredSalathLangs.map((lang) => ({
lang,
text: PrayerTimes.translations[salath]?.[lang] ?? salath
}));
return {
primary: names[0],
secondary: names.slice(1)
};
}
});
class PrayerTimes {
static salaths = ["fajr", "sunrise", "dhuhr", "asr", "maghrib", "isha"];
static translations = {
fajr: {
tr: "İmsak",
en: "Fajr",
de: "Fruehgebet",
ar: "صلاة الفجر"
},
sunrise: {
tr: "Güneş",
en: "Sunrise",
de: "Sonnenaufgang",
ar: "الشروق"
},
dhuhr: {
tr: "Öğle",
en: "Dhuhr",
de: "Mittagsgebet",
ar: "صلاة الظهر"
},
asr: {
tr: "İkindi",
en: "Asr",
de: "Nachmittagsgebet",
ar: "صلاة العصر"
},
maghrib: {
tr: "Akşam",
en: "Maghrib",
de: "Abendgebet",
ar: "صلاة المغرب"
},
isha: {
tr: "Yatsı",
en: "Isha",
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 totalSeconds = Math.max(0, Math.floor(seconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}
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)
);
});
}
function normalizeLanguageTag(tag) {
return String(tag).toLowerCase().split("-")[0];
}