feat: Add Diyanet API provider

feat: Add UI
This commit is contained in:
2023-03-05 14:41:38 +01:00
parent d4ff42387f
commit 93af84cef8
17 changed files with 642 additions and 43 deletions
+1
View File
@@ -0,0 +1 @@
(()=>{function d(t){let e=()=>{let n,l=localStorage;return t.interceptor((r,i,s,a,m)=>{let o=n||`_x_${a}`,u=f(o,l)?g(o,l):r;return s(u),t.effect(()=>{let c=i();p(o,c,l),s(c)}),u},r=>{r.as=i=>(n=i,r),r.using=i=>(l=i,r)})};Object.defineProperty(t,"$persist",{get:()=>e()}),t.magic("persist",e),t.persist=(n,{get:l,set:r},i=localStorage)=>{let s=f(n,i)?g(n,i):l();r(s),t.effect(()=>{let a=l();p(n,a,i),r(a)})}}function f(t,e){return e.getItem(t)!==null}function g(t,e){return JSON.parse(e.getItem(t,e))}function p(t,e,n){n.setItem(t,JSON.stringify(e))}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(d)});})();
File diff suppressed because one or more lines are too long
+98
View File
@@ -0,0 +1,98 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport'
content='width=device-width, initial-scale=1.0'>
<title>Document</title>
<script defer
src='main.js'></script>
<script defer
src='alpine.persist@3.x.x.min.js'></script>
<script defer
src='alpine@3.11.1.min.js'></script>
<link rel='preconnect'
href='https://fonts.googleapis.com'>
<link rel='preconnect'
href='https://fonts.gstatic.com'
crossorigin>
<link href='https://fonts.googleapis.com/css2?family=Inter:wght@400..900&family=Noto+Sans+Arabic:wght@400..700&display=swap'
rel='stylesheet'>
<link rel='stylesheet'
href='style.css'>
</head>
<body>
<div x-data='app()'
@hashchange.window='onHash'>
<template x-if='debug'>
<div>
<label>clock: <input type='range'
step='1'
min='0'
max='1439'
x-model='userMinutes'> <span x-text='userTime'></span></label>
</div>
</template>
<input type='text'
x-model='locationId'>
<template x-if='todayTimes'>
<div class='current-salath text--center'>
<p>
<time :datetime='userNow'
x-text='formatDate(userNow)'></time>
</p>
<p>
<span class='salath-name'
lang='tr'
x-text='todayTimes.currentSalath.name("tr")'></span>
<span class='salath-name'
lang='de'
x-text='todayTimes.currentSalath.name("de")'></span>
<span class='salath-name text--arabic'
lang='ar'
x-text='todayTimes.currentSalath.name("ar")'></span>
</p>
<p>
<time :datetime='todayTimes.nextSalath.startsAt'
class='text--numeric text--time'
x-text='todayTimes.nextSalath.untilHuman'></time>
</p>
</div>
</template>
<template x-if='todayTimes'>
<div class='table-wrapper'>
<table class='salath-table'>
<tbody>
<template x-for='it in todayTimes.times'>
<tr class='salath'
:class='{current: it.salath === todayTimes.currentSalath.salath}'>
<td class='cell-names'>
<span class='salath-name'
x-text='it.name("tr")'></span>
<span class='salath-name'
x-text='it.name("de")'></span>
<span class='salath-name text--arabic'
x-text='it.name("ar")'></span>
</td>
<td class='cell-times'>
<span class='salath-time text--time text--numeric'
x-text='it.timeLocal'></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
<div class='text--center text--numeric'>
<p><span class='clock'
x-text='formatTime(now)'></span></p>
</div>
</div>
</body>
</html>
+208
View File
@@ -0,0 +1,208 @@
const app = () => ({
futureTimes: [],
locationId: Alpine.$persist('11104'),
lastUpdated: Alpine.$persist(null),
userMinutes: 0,
now: new Date(),
debug: location.hash === '#debug',
geolocation: null,
async init() {
await this.refreshIfStale();
setInterval(() => {
this.now = new Date();
}, 500);
getUserLocation()
.then(loc => {
this.geolocation = {latitude: loc.latitude, longitude: loc.longitude};
this.refreshIfStale();
})
.catch(() => this.geolocation = null)
},
onHash() {
this.debug = location.hash === '#debug'
},
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);
},
async refreshIfStale() {
const updatedAt = new Date(this.lastUpdated);
const now = new Date();
const elapsedSeconds = (now - updatedAt) / 1000;
if (this.geolocation !== null) {
this.futureTimes = await fetchJSON(`/api/v1/diyanet/prayertimes?latitude=${this.geolocation.latitude}&longitude=${this.geolocation.longitude}`);
} else {
this.futureTimes = await fetchJSON(`/api/v1/diyanet/prayertimes?location_id=${this.locationId}`);
}
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 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: 'صلاة العشاء'},
}
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 => {
// "2023-03-05T00:00:00Z"
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() {
let 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) {
// 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
}
get nextSalath() {
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);
next = new PrayerTimes({date: nextDay.toISOString(), ...this.salathTimes}, this.clock).times[0];
}
return {
...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",
minute: "numeric",
second: "numeric"
}).format(then);
}
/**
* @param {Date} then
* @return {string}
* */
function formatDate(then) {
return new Intl.DateTimeFormat(navigator.language, {
year: "numeric",
month: "2-digit",
day: "2-digit"
}).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()
}
/**
* @return {Promise<GeolocationCoordinates>}
* */
function getUserLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject("Geolocation is not supported by this browser.");
return;
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position.coords),
(error) => reject(error.message)
);
});
}
+88
View File
@@ -0,0 +1,88 @@
*, *:before, *:after {
box-sizing: border-box;
}
body {
line-height: 1.6;
font-family: 'Inter', sans-serif;
}
p {
margin: 0;
}
p + p {
margin-top: 1rem;
}
.text--numeric {
font-variant: tabular-nums;
letter-spacing: -0.03em;
}
.text--center {
text-align: center;
}
.text--arabic {
font-family: 'Noto Sans Arabic', Inter, sans-serif;
line-height: 1;
}
.text--time {
font-size: 1.5em;
}
.table-wrapper {
padding: 0 2rem;
max-width: 66%;
margin: auto;
}
.salath-table {
font-size: 2rem;
font-weight: 600;
width: 100%;
vertical-align: middle;
}
td {
padding: 0.25rem 0.5rem;
}
.cell-names {
white-space: nowrap;
}
.cell-times {
white-space: nowrap;
text-align: right;
}
.salath-name {
}
.current {
color: tomato;
}
.salath-name + .salath-name:before {
content: '/';
opacity: 0.25;
margin: 0 0.5em;
}
.salath-time {
font-weight: 700;
}
.clock {
font-size: 4rem;
font-weight: bold;
}
.current-salath {
font-weight: bold;
font-size: 1.5rem;
}
+24
View File
@@ -0,0 +1,24 @@
package templates
import (
"embed"
"io/fs"
"net/http"
"os"
)
//go:embed static
var staticFS embed.FS
func FS() fs.FS {
if os.Getenv("DEBUG") == "1" {
return os.DirFS("./templates/static")
}
f, _ := fs.Sub(staticFS, "static")
return f
}
func HttpFS() http.FileSystem {
return http.FS(FS())
}