Phase 1: Prayer times, Qibla, Quran, Hijri, Tasbih, 99 Names PWA
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
<script>
|
||||
import PrayerTimes from './lib/PrayerTimes.svelte';
|
||||
import Qibla from './lib/Qibla.svelte';
|
||||
import Quran from './lib/Quran.svelte';
|
||||
import Hijri from './lib/Hijri.svelte';
|
||||
import Tasbih from './lib/Tasbih.svelte';
|
||||
import Names99 from './lib/Names99.svelte';
|
||||
|
||||
const tabs = ['Prayer', 'Qibla', 'Quran', 'Hijri', 'Tasbih', '99 Names'];
|
||||
const icons = ['🕌', '🧭', '📖', '📅', '📿', '💚'];
|
||||
let activeTab = $state(0);
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === 'ArrowRight') activeTab = (activeTab + 1) % tabs.length;
|
||||
if (e.key === 'ArrowLeft') activeTab = (activeTab - 1 + tabs.length) % tabs.length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="app">
|
||||
<header>
|
||||
<h1>Nūr</h1>
|
||||
<span class="subtitle">Muslim Companion</span>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
{#each tabs as tab, i}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeTab === i}
|
||||
onclick={() => activeTab = i}
|
||||
aria-label={tab}
|
||||
>
|
||||
<span class="tab-icon">{icons[i]}</span>
|
||||
<span class="tab-label">{tab}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{#if activeTab === 0}
|
||||
<PrayerTimes />
|
||||
{:else if activeTab === 1}
|
||||
<Qibla />
|
||||
{:else if activeTab === 2}
|
||||
<Quran />
|
||||
{:else if activeTab === 3}
|
||||
<Hijri />
|
||||
{:else if activeTab === 4}
|
||||
<Tasbih />
|
||||
{:else if activeTab === 5}
|
||||
<Names99 />
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(*) { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:global(body) {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f0fdfa;
|
||||
color: #134e4a;
|
||||
min-height: 100dvh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.app {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 20px 16px 8px;
|
||||
background: linear-gradient(135deg, #0f766e, #0d9488);
|
||||
color: white;
|
||||
border-radius: 0 0 24px 24px;
|
||||
}
|
||||
header h1 { font-size: 1.8rem; font-weight: 700; }
|
||||
.subtitle { font-size: 0.8rem; opacity: 0.85; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 8px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
color: #5b8c85;
|
||||
transition: all 0.2s;
|
||||
min-width: 56px;
|
||||
}
|
||||
.tab.active {
|
||||
background: #ccfbf1;
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab-icon { font-size: 1.3rem; }
|
||||
.tab-label { white-space: nowrap; }
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 0 12px 16px;
|
||||
}
|
||||
|
||||
:global(.card) {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 3px rgba(15, 118, 110, 0.08);
|
||||
}
|
||||
:global(.card h2) {
|
||||
font-size: 1rem;
|
||||
color: #0f766e;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
:global(button.primary) {
|
||||
background: linear-gradient(135deg, #0f766e, #0d9488);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
:global(button.primary:active) { opacity: 0.85; }
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0f766e" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Nūr — Muslim Companion</title>
|
||||
<link rel="preconnect" href="https://api.alquran.cloud" crossorigin />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script>
|
||||
let hijriDate = $state(null);
|
||||
let gregorian = $state('');
|
||||
let loading = $state(true);
|
||||
|
||||
async function loadHijri() {
|
||||
try {
|
||||
const today = new Date();
|
||||
const d = String(today.getDate()).padStart(2, '0');
|
||||
const m = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const y = today.getFullYear();
|
||||
gregorian = `${d}/${m}/${y}`;
|
||||
const url = `https://api.aladhan.com/v1/gToH/${d}-${m}-${y}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.code === 200) {
|
||||
hijriDate = data.data.hijri;
|
||||
}
|
||||
loading = false;
|
||||
} catch (e) {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => { loadHijri(); });
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>📅 Hijri Calendar</h2>
|
||||
|
||||
{#if loading}
|
||||
<p style="color:#5b8c85">⏳ Loading…</p>
|
||||
{:else if hijriDate}
|
||||
<div class="date-display">
|
||||
<div class="hijri-big">
|
||||
<span class="hijri-day">{hijriDate.day}</span>
|
||||
<span class="hijri-month">{hijriDate.month.en}</span>
|
||||
<span class="hijri-year">{hijriDate.year} AH</span>
|
||||
</div>
|
||||
<div class="gregorian-row">
|
||||
<span>Gregorian: {gregorian}</span>
|
||||
</div>
|
||||
<div class="hijri-info">
|
||||
<div class="info-row"><span>Weekday</span><span>{hijriDate.weekday.en}</span></div>
|
||||
<div class="info-row"><span>Month #</span><span>{hijriDate.month.number}</span></div>
|
||||
<div class="info-row"><span>Days in month</span><span>{hijriDate.month.days}</span></div>
|
||||
<div class="info-row"><span>Designation</span><span>{hijriDate.designation.expanded}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.date-display { display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
.hijri-big {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #0f766e, #0d9488);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px 32px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.hijri-day { font-size: 2.8rem; font-weight: 700; line-height: 1; }
|
||||
.hijri-month { font-size: 1.2rem; opacity: 0.9; margin-top: 4px; }
|
||||
.hijri-year { font-size: 0.9rem; opacity: 0.75; margin-top: 2px; }
|
||||
.gregorian-row { font-size: 0.85rem; color: #5b8c85; }
|
||||
.hijri-info { width: 100%; display: flex; flex-direction: column; gap: 4px; }
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #f0fdfa;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #134e4a;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<script>
|
||||
const names = [
|
||||
{ no: 1, ar: 'الرَّحْمَنُ', en: 'Ar-Rahman', meaning: 'The Most Merciful' },
|
||||
{ no: 2, ar: 'الرَّحِيمُ', en: 'Ar-Raheem', meaning: 'The Most Compassionate' },
|
||||
{ no: 3, ar: 'الْمَلِكُ', en: 'Al-Malik', meaning: 'The King' },
|
||||
{ no: 4, ar: 'الْقُدُّوسُ', en: 'Al-Quddus', meaning: 'The Most Holy' },
|
||||
{ no: 5, ar: 'السَّلَامُ', en: 'As-Salam', meaning: 'The Source of Peace' },
|
||||
{ no: 6, ar: 'الْمُؤْمِنُ', en: 'Al-Mu\'min', meaning: 'The Guardian of Faith' },
|
||||
{ no: 7, ar: 'الْمُهَيْمِنُ', en: 'Al-Muhaymin', meaning: 'The Protector' },
|
||||
{ no: 8, ar: 'الْعَزِيزُ', en: 'Al-Aziz', meaning: 'The Almighty' },
|
||||
{ no: 9, ar: 'الْجَبَّارُ', en: 'Al-Jabbar', meaning: 'The Compeller' },
|
||||
{ no: 10, ar: 'الْمُتَكَبِّرُ', en: 'Al-Mutakabbir', meaning: 'The Supreme' },
|
||||
{ no: 11, ar: 'الْخَالِقُ', en: 'Al-Khaliq', meaning: 'The Creator' },
|
||||
{ no: 12, ar: 'الْبَارِئُ', en: 'Al-Bari', meaning: 'The Maker' },
|
||||
{ no: 13, ar: 'الْمُصَوِّرُ', en: 'Al-Musawwir', meaning: 'The Fashioner' },
|
||||
{ no: 14, ar: 'الْغَفَّارُ', en: 'Al-Ghaffar', meaning: 'The Forgiving' },
|
||||
{ no: 15, ar: 'الْقَهَّارُ', en: 'Al-Qahhar', meaning: 'The Subduer' },
|
||||
{ no: 16, ar: 'الْوَهَّابُ', en: 'Al-Wahhab', meaning: 'The Bestower' },
|
||||
{ no: 17, ar: 'الرَّزَّاقُ', en: 'Ar-Razzaq', meaning: 'The Provider' },
|
||||
{ no: 18, ar: 'الْفَتَّاحُ', en: 'Al-Fattah', meaning: 'The Opener' },
|
||||
{ no: 19, ar: 'الْعَلِيمُ', en: 'Al-'Aleem', meaning: 'The All-Knowing' },
|
||||
{ no: 20, ar: 'الْقَابِضُ', en: 'Al-Qabid', meaning: 'The Constrictor' },
|
||||
{ no: 21, ar: 'الْبَاسِطُ', en: 'Al-Basit', meaning: 'The Expander' },
|
||||
{ no: 22, ar: 'الْخَافِضُ', en: 'Al-Khafid', meaning: 'The Abaser' },
|
||||
{ no: 23, ar: 'الرَّافِعُ', en: 'Ar-Rafi', meaning: 'The Exalter' },
|
||||
{ no: 24, ar: 'الْمُعِزُّ', en: 'Al-Mu\'izz', meaning: 'The Bestower of Honor' },
|
||||
{ no: 25, ar: 'الْمُذِلُّ', en: 'Al-Mudhill', meaning: 'The Humiliator' },
|
||||
{ no: 26, ar: 'السَّمِيعُ', en: 'As-Sami', meaning: 'The All-Hearing' },
|
||||
{ no: 27, ar: 'الْبَصِيرُ', en: 'Al-Basir', meaning: 'The All-Seeing' },
|
||||
{ no: 28, ar: 'الْحَكَمُ', en: 'Al-Hakam', meaning: 'The Judge' },
|
||||
{ no: 29, ar: 'الْعَدْلُ', en: 'Al-'Adl', meaning: 'The Just' },
|
||||
{ no: 30, ar: 'اللَّطِيفُ', en: 'Al-Latif', meaning: 'The Subtle One' },
|
||||
{ no: 31, ar: 'الْخَبِيرُ', en: 'Al-Khabir', meaning: 'The All-Aware' },
|
||||
{ no: 32, ar: 'الْحَلِيمُ', en: 'Al-Halim', meaning: 'The Forbearing' },
|
||||
{ no: 33, ar: 'الْعَظِيمُ', en: 'Al-'Azim', meaning: 'The Magnificent' },
|
||||
{ no: 34, ar: 'الْغَفُورُ', en: 'Al-Ghafur', meaning: 'The All-Forgiving' },
|
||||
{ no: 35, ar: 'الشَّكُورُ', en: 'Ash-Shakur', meaning: 'The Appreciative' },
|
||||
{ no: 36, ar: 'الْعَلِيُّ', en: 'Al-'Aliyy', meaning: 'The Most High' },
|
||||
{ no: 37, ar: 'الْكَبِيرُ', en: 'Al-Kabir', meaning: 'The Most Great' },
|
||||
{ no: 38, ar: 'الْحَفِيظُ', en: 'Al-Hafiz', meaning: 'The Preserver' },
|
||||
{ no: 39, ar: 'الْمُقِيتُ', en: 'Al-Muqit', meaning: 'The Sustainer' },
|
||||
{ no: 40, ar: 'الْحَسِيبُ', en: 'Al-Hasib', meaning: 'The Reckoner' },
|
||||
{ no: 41, ar: 'الْجَلِيلُ', en: 'Al-Jalil', meaning: 'The Majestic' },
|
||||
{ no: 42, ar: 'الْكَرِيمُ', en: 'Al-Karim', meaning: 'The Generous' },
|
||||
{ no: 43, ar: 'الرَّقِيبُ', en: 'Ar-Raqib', meaning: 'The Watchful' },
|
||||
{ no: 44, ar: 'الْمُجِيبُ', en: 'Al-Mujib', meaning: 'The Responsive' },
|
||||
{ no: 45, ar: 'الْوَاسِعُ', en: 'Al-Wasi', meaning: 'The All-Encompassing' },
|
||||
{ no: 46, ar: 'الْحَكِيمُ', en: 'Al-Hakim', meaning: 'The All-Wise' },
|
||||
{ no: 47, ar: 'الْوَدُودُ', en: 'Al-Wadud', meaning: 'The Loving' },
|
||||
{ no: 48, ar: 'الْمَجِيدُ', en: 'Al-Majid', meaning: 'The Glorious' },
|
||||
{ no: 49, ar: 'الْبَاعِثُ', en: 'Al-Ba\'ith', meaning: 'The Resurrector' },
|
||||
{ no: 50, ar: 'الشَّهِيدُ', en: 'Ash-Shahid', meaning: 'The Witness' },
|
||||
{ no: 51, ar: 'الْحَقُّ', en: 'Al-Haqq', meaning: 'The Truth' },
|
||||
{ no: 52, ar: 'الْوَكِيلُ', en: 'Al-Wakil', meaning: 'The Trustee' },
|
||||
{ no: 53, ar: 'الْقَوِيُّ', en: 'Al-Qawiyy', meaning: 'The Most Strong' },
|
||||
{ no: 54, ar: 'الْمَتِينُ', en: 'Al-Matin', meaning: 'The Firm' },
|
||||
{ no: 55, ar: 'الْوَلِيُّ', en: 'Al-Waliyy', meaning: 'The Protecting Friend' },
|
||||
{ no: 56, ar: 'الْحَمِيدُ', en: 'Al-Hamid', meaning: 'The Praiseworthy' },
|
||||
{ no: 57, ar: 'الْمُحْصِي', en: 'Al-Muhsi', meaning: 'The Reckoner' },
|
||||
{ no: 58, ar: 'الْمُبْدِئُ', en: 'Al-Mubdi', meaning: 'The Originator' },
|
||||
{ no: 59, ar: 'الْمُعِيدُ', en: 'Al-Mu\'id', meaning: 'The Restorer' },
|
||||
{ no: 60, ar: 'الْمُحْيِي', en: 'Al-Muhyi', meaning: 'The Giver of Life' },
|
||||
{ no: 61, ar: 'الْمُمِيتُ', en: 'Al-Mumit', meaning: 'The Taker of Life' },
|
||||
{ no: 62, ar: 'الْحَيُّ', en: 'Al-Hayy', meaning: 'The Ever-Living' },
|
||||
{ no: 63, ar: 'الْقَيُّومُ', en: 'Al-Qayyum', meaning: 'The Sustainer' },
|
||||
{ no: 64, ar: 'الْوَاجِدُ', en: 'Al-Wajid', meaning: 'The Finder' },
|
||||
{ no: 65, ar: 'الْمَاجِدُ', en: 'Al-Majid', meaning: 'The Noble' },
|
||||
{ no: 66, ar: 'الْوَاحِدُ', en: 'Al-Wahid', meaning: 'The One' },
|
||||
{ no: 67, ar: 'الْأَحَدُ', en: 'Al-Ahad', meaning: 'The Unique' },
|
||||
{ no: 68, ar: 'الصَّمَدُ', en: 'As-Samad', meaning: 'The Eternal' },
|
||||
{ no: 69, ar: 'الْقَادِرُ', en: 'Al-Qadir', meaning: 'The Able' },
|
||||
{ no: 70, ar: 'الْمُقْتَدِرُ', en: 'Al-Muqtadir', meaning: 'The Omnipotent' },
|
||||
{ no: 71, ar: 'الْمُقَدِّمُ', en: 'Al-Muqaddim', meaning: 'The Advancer' },
|
||||
{ no: 72, ar: 'الْمُؤَخِّرُ', en: 'Al-Mu\'akhkhir', meaning: 'The Delayer' },
|
||||
{ no: 73, ar: 'الْأَوَّلُ', en: 'Al-Awwal', meaning: 'The First' },
|
||||
{ no: 74, ar: 'الْآخِرُ', en: 'Al-Akhir', meaning: 'The Last' },
|
||||
{ no: 75, ar: 'الظَّاهِرُ', en: 'Az-Zahir', meaning: 'The Manifest' },
|
||||
{ no: 76, ar: 'الْبَاطِنُ', en: 'Al-Batin', meaning: 'The Hidden' },
|
||||
{ no: 77, ar: 'الْوَالِي', en: 'Al-Wali', meaning: 'The Governor' },
|
||||
{ no: 78, ar: 'الْمُتَعَالِي', en: 'Al-Muta\'ali', meaning: 'The Most Exalted' },
|
||||
{ no: 79, ar: 'الْبَرُّ', en: 'Al-Barr', meaning: 'The Source of Goodness' },
|
||||
{ no: 80, ar: 'التَّوَّابُ', en: 'At-Tawwab', meaning: 'The Acceptor of Repentance' },
|
||||
{ no: 81, ar: 'الْمُنْتَقِمُ', en: 'Al-Muntaqim', meaning: 'The Avenger' },
|
||||
{ no: 82, ar: 'الْعَفُوُّ', en: 'Al-'Afuww', meaning: 'The Pardoner' },
|
||||
{ no: 83, ar: 'الرَّؤُوفُ', en: 'Ar-Ra\'uf', meaning: 'The Kind' },
|
||||
{ no: 84, ar: 'مَالِكُ الْمُلْكِ', en: 'Malik-ul-Mulk', meaning: 'Master of the Kingdom' },
|
||||
{ no: 85, ar: 'ذُو الْجَلَالِ وَالْإِكْرَامِ', en: 'Dhul-Jalal wal-Ikram', meaning: 'Lord of Majesty & Honor' },
|
||||
{ no: 86, ar: 'الْمُقْسِطُ', en: 'Al-Muqsit', meaning: 'The Equitable' },
|
||||
{ no: 87, ar: 'الْجَامِعُ', en: 'Al-Jami', meaning: 'The Gatherer' },
|
||||
{ no: 88, ar: 'الْغَنِيُّ', en: 'Al-Ghaniyy', meaning: 'The Self-Sufficient' },
|
||||
{ no: 89, ar: 'الْمُغْنِي', en: 'Al-Mughni', meaning: 'The Enricher' },
|
||||
{ no: 90, ar: 'الْمَانِعُ', en: 'Al-Mani', meaning: 'The Preventer' },
|
||||
{ no: 91, ar: 'الضَّارُّ', en: 'Ad-Darr', meaning: 'The Afflictor' },
|
||||
{ no: 92, ar: 'النَّافِعُ', en: 'An-Nafi', meaning: 'The Benefactor' },
|
||||
{ no: 93, ar: 'النُّورُ', en: 'An-Nur', meaning: 'The Light' },
|
||||
{ no: 94, ar: 'الْهَادِي', en: 'Al-Hadi', meaning: 'The Guide' },
|
||||
{ no: 95, ar: 'الْبَدِيعُ', en: 'Al-Badi', meaning: 'The Originator' },
|
||||
{ no: 96, ar: 'الْبَاقِي', en: 'Al-Baqi', meaning: 'The Everlasting' },
|
||||
{ no: 97, ar: 'الْوَارِثُ', en: 'Al-Warith', meaning: 'The Inheritor' },
|
||||
{ no: 98, ar: 'الرَّشِيدُ', en: 'Ar-Rashid', meaning: 'The Righteous Guide' },
|
||||
{ no: 99, ar: 'الصَّبُورُ', en: 'As-Sabur', meaning: 'The Patient' },
|
||||
];
|
||||
|
||||
let search = $state('');
|
||||
let expanded = $state(null);
|
||||
|
||||
const filtered = $derived(
|
||||
search.trim()
|
||||
? names.filter(n => n.en.toLowerCase().includes(search.toLowerCase()) || n.meaning.toLowerCase().includes(search.toLowerCase()) || n.ar.includes(search))
|
||||
: names
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>💚 99 Names of Allah</h2>
|
||||
|
||||
<input
|
||||
class="search-input"
|
||||
type="search"
|
||||
placeholder="Search names…"
|
||||
bind:value={search}
|
||||
/>
|
||||
|
||||
<div class="names-grid">
|
||||
{#each filtered as n}
|
||||
<button
|
||||
class="name-card"
|
||||
class:expanded={expanded === n.no}
|
||||
onclick={() => expanded = expanded === n.no ? null : n.no}
|
||||
>
|
||||
<span class="name-no">{n.no}</span>
|
||||
<div class="name-body">
|
||||
<span class="name-ar">{n.ar}</span>
|
||||
<span class="name-en">{n.en}</span>
|
||||
{#if expanded === n.no}
|
||||
<span class="name-meaning">{n.meaning}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #99d6c9;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 12px;
|
||||
color: #134e4a;
|
||||
outline: none;
|
||||
}
|
||||
.search-input:focus { border-color: #0f766e; }
|
||||
.names-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.name-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
background: #f0fdfa;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.name-card:active { background: #ccfbf1; }
|
||||
.name-no {
|
||||
background: #0f766e;
|
||||
color: white;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.name-body { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||
.name-ar {
|
||||
font-size: 1rem;
|
||||
color: #134e4a;
|
||||
font-weight: 600;
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
.name-en { font-size: 0.8rem; color: #0f766e; font-weight: 500; }
|
||||
.name-meaning { font-size: 0.75rem; color: #5b8c85; margin-top: 2px; }
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script>
|
||||
let lat = $state(null);
|
||||
let lng = $state(null);
|
||||
let prayers = $state(null);
|
||||
let nextPrayer = $state(null);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let method = $state(3); // Muslim World League
|
||||
let geoLoading = $state(true);
|
||||
|
||||
const methods = [
|
||||
{ id: 3, name: 'Muslim World League' },
|
||||
{ id: 2, name: 'ISNA (N. America)' },
|
||||
{ id: 4, name: 'Umm al-Qura' },
|
||||
{ id: 5, name: 'Egyptian' },
|
||||
{ id: 1, name: 'Karachi University' },
|
||||
];
|
||||
|
||||
function getLocation() {
|
||||
if (!navigator.geolocation) {
|
||||
error = 'Geolocation not supported';
|
||||
loading = false;
|
||||
geoLoading = false;
|
||||
return;
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
lat = pos.coords.latitude;
|
||||
lng = pos.coords.longitude;
|
||||
geoLoading = false;
|
||||
fetchPrayers();
|
||||
},
|
||||
() => {
|
||||
// Default to Makkah if location denied
|
||||
lat = 21.4225;
|
||||
lng = 39.8262;
|
||||
geoLoading = false;
|
||||
fetchPrayers();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchPrayers() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const url = `https://api.aladhan.com/v1/timings/${today}?latitude=${lat}&longitude=${lng}&method=${method}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.code === 200) {
|
||||
prayers = data.data.timings;
|
||||
nextPrayer = findNextPrayer(data.data.timings, data.data.meta.timezone);
|
||||
loading = false;
|
||||
} else {
|
||||
error = 'Could not load prayer times';
|
||||
loading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Network error — check connection';
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function findNextPrayer(timings, tz) {
|
||||
const now = new Date();
|
||||
const names = ['Fajr', 'Sunrise', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
|
||||
for (const name of names) {
|
||||
const [h, m] = timings[name].split(':').map(Number);
|
||||
const pt = new Date(now);
|
||||
pt.setHours(h, m, 0, 0);
|
||||
if (pt > now) return { name, time: timings[name] };
|
||||
}
|
||||
return { name: 'Fajr', time: timings['Fajr'] + ' (tomorrow)' };
|
||||
}
|
||||
|
||||
$effect(() => { getLocation(); });
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>🕌 Prayer Times</h2>
|
||||
|
||||
{#if geoLoading}
|
||||
<p style="color:#5b8c85">📍 Getting location…</p>
|
||||
{:else if loading}
|
||||
<p style="color:#5b8c85">⏳ Loading prayer times…</p>
|
||||
{:else if error}
|
||||
<p style="color:#e11d48">{error}</p>
|
||||
<button class="primary" onclick={fetchPrayers}>Retry</button>
|
||||
{:else if prayers}
|
||||
{#if nextPrayer}
|
||||
<div class="next-prayer">
|
||||
<span class="next-label">Next: {nextPrayer.name}</span>
|
||||
<span class="next-time">{nextPrayer.time}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="prayer-grid">
|
||||
{#each ['Fajr', 'Sunrise', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'] as name}
|
||||
<div class="prayer-row" class:active={nextPrayer?.name === name}>
|
||||
<span class="prayer-name">{name}</span>
|
||||
<span class="prayer-time">{prayers[name]}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="method-select">
|
||||
<label for="method">Calculation method:</label>
|
||||
<select id="method" bind:value={method} onchange={fetchPrayers}>
|
||||
{#each methods as m}
|
||||
<option value={m.id}>{m.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.next-prayer {
|
||||
background: linear-gradient(135deg, #0f766e, #0d9488);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.next-label { font-size: 0.8rem; opacity: 0.9; }
|
||||
.next-time { font-size: 1.6rem; font-weight: 700; }
|
||||
.prayer-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.prayer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: #f0fdfa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.prayer-row.active {
|
||||
background: #ccfbf1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.prayer-name { color: #0f766e; }
|
||||
.prayer-time { font-variant-numeric: tabular-nums; }
|
||||
.method-select {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: #5b8c85;
|
||||
}
|
||||
.method-select select {
|
||||
border: 1px solid #99d6c9;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
color: #0f766e;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script>
|
||||
let bearing = $state(0);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let deviceAlpha = $state(0);
|
||||
|
||||
const KAABA = { lat: 21.4225, lng: 39.8262 };
|
||||
|
||||
function toRad(d) { return (d * Math.PI) / 180; }
|
||||
function toDeg(r) { return (r * 180) / Math.PI; }
|
||||
|
||||
function calcBearing(lat, lng) {
|
||||
const dLng = toRad(KAABA.lng - lng);
|
||||
const y = Math.sin(dLng) * Math.cos(toRad(KAABA.lat));
|
||||
const x = Math.cos(toRad(lat)) * Math.sin(toRad(KAABA.lat))
|
||||
- Math.sin(toRad(lat)) * Math.cos(toRad(KAABA.lat)) * Math.cos(dLng);
|
||||
return (toDeg(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
function startCompass() {
|
||||
if (!navigator.geolocation) {
|
||||
error = 'Geolocation not supported';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
bearing = calcBearing(pos.coords.latitude, pos.coords.longitude);
|
||||
loading = false;
|
||||
},
|
||||
() => {
|
||||
bearing = calcBearing(3.1390, 101.6869); // KL default
|
||||
loading = false;
|
||||
}
|
||||
);
|
||||
|
||||
if (window.DeviceOrientationEvent) {
|
||||
window.addEventListener('deviceorientation', (e) => {
|
||||
if (e.webkitCompassHeading !== undefined) {
|
||||
deviceAlpha = e.webkitCompassHeading;
|
||||
} else if (e.alpha !== null) {
|
||||
deviceAlpha = 360 - e.alpha;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => { startCompass(); });
|
||||
|
||||
const needleAngle = $derived(bearing - deviceAlpha);
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>🧭 Qibla Finder</h2>
|
||||
|
||||
{#if loading}
|
||||
<p style="color:#5b8c85">📍 Getting location…</p>
|
||||
{:else if error}
|
||||
<p style="color:#e11d48">{error}</p>
|
||||
{:else}
|
||||
<div class="compass-box">
|
||||
<div class="compass" style="transform: rotate({needleAngle}deg)">
|
||||
<div class="needle">
|
||||
<svg viewBox="0 0 60 80" width="40" height="60">
|
||||
<polygon points="30,0 0,80 30,60 60,80" fill="#0f766e" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bearing-info">
|
||||
<span class="bearing-num">{Math.round(bearing)}°</span>
|
||||
<span class="bearing-dir">from North</span>
|
||||
</div>
|
||||
<p class="compass-hint">{deviceAlpha === 0 ? '🗺️ Move your device to calibrate compass' : '↗️ Point the needle towards the Kaaba'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.compass-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.compass {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: #f0fdfa;
|
||||
border: 3px solid #0f766e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.needle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bearing-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.bearing-num { font-size: 2rem; font-weight: 700; color: #0f766e; }
|
||||
.bearing-dir { font-size: 0.8rem; color: #5b8c85; }
|
||||
.compass-hint { font-size: 0.75rem; color: #94a3a8; text-align: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script>
|
||||
let surahs = $state([]);
|
||||
let selectedSurah = $state(null);
|
||||
let ayahs = $state([]);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
|
||||
async function loadSurahs() {
|
||||
try {
|
||||
const res = await fetch('https://api.alquran.cloud/v1/surah');
|
||||
const data = await res.json();
|
||||
if (data.code === 200) surahs = data.data;
|
||||
loading = false;
|
||||
} catch (e) {
|
||||
error = 'Network error';
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSurah(num) {
|
||||
loading = true;
|
||||
try {
|
||||
const res = await fetch(`https://api.alquran.cloud/v1/surah/${num}/en.asad`);
|
||||
const data = await res.json();
|
||||
if (data.code === 200) {
|
||||
selectedSurah = { num, name: data.data.englishName, english: data.data.englishNameTranslation, type: data.data.revelationType };
|
||||
ayahs = data.data.ayahs;
|
||||
}
|
||||
loading = false;
|
||||
} catch (e) {
|
||||
error = 'Failed to load surah';
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
selectedSurah = null;
|
||||
ayahs = [];
|
||||
}
|
||||
|
||||
$effect(() => { loadSurahs(); });
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>📖 Holy Quran</h2>
|
||||
|
||||
{#if loading}
|
||||
<p style="color:#5b8c85">⏳ Loading…</p>
|
||||
{:else if error}
|
||||
<p style="color:#e11d48">{error}</p>
|
||||
{:else if selectedSurah}
|
||||
<div class="surah-header">
|
||||
<button class="back-btn" onclick={goBack}>← Surahs</button>
|
||||
<h3>{selectedSurah.name} ({selectedSurah.english})</h3>
|
||||
<span class="surah-type">{selectedSurah.type}</span>
|
||||
</div>
|
||||
<div class="ayah-list">
|
||||
{#each ayahs as ayah}
|
||||
<div class="ayah">
|
||||
<span class="ayah-num">{ayah.numberInSurah}</span>
|
||||
<p class="ayah-text">{ayah.text}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="surah-grid">
|
||||
{#each surahs as s}
|
||||
<button class="surah-btn" onclick={() => loadSurah(s.number)}>
|
||||
<span class="surah-num">{s.number}</span>
|
||||
<span class="surah-name">{s.englishName}</span>
|
||||
<span class="surah-ayahs">{s.numberOfAyahs} ayahs</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.surah-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.surah-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: none;
|
||||
background: #f0fdfa;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.surah-btn:active { background: #ccfbf1; }
|
||||
.surah-num {
|
||||
background: #0f766e;
|
||||
color: white;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.surah-name { font-weight: 600; color: #134e4a; flex: 1; }
|
||||
.surah-ayahs { font-size: 0.7rem; color: #5b8c85; }
|
||||
.surah-header { margin-bottom: 12px; }
|
||||
.surah-header h3 { font-size: 1.1rem; color: #0f766e; margin: 4px 0; }
|
||||
.surah-type { font-size: 0.7rem; color: #5b8c85; background: #ccfbf1; padding: 2px 8px; border-radius: 4px; }
|
||||
.back-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.ayah-list { max-height: 60vh; overflow-y: auto; }
|
||||
.ayah {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e6f5f0;
|
||||
}
|
||||
.ayah-num {
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ayah-text { font-size: 0.9rem; line-height: 1.6; color: #134e4a; }
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let total = $state(0);
|
||||
let target = $state(33);
|
||||
let vibrate = $state(true);
|
||||
|
||||
function increment() {
|
||||
count = (count + 1) % target;
|
||||
total++;
|
||||
if (vibrate && navigator.vibrate) navigator.vibrate(15);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
count = 0;
|
||||
total = 0;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
function handleSpace(e) {
|
||||
if (e.code === 'Space' && e.target === document.body) {
|
||||
e.preventDefault();
|
||||
increment();
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleSpace);
|
||||
return () => window.removeEventListener('keydown', handleSpace);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>📿 Tasbih Counter</h2>
|
||||
|
||||
<div class="tasbih-display">
|
||||
<div class="counter-ring" onclick={increment}>
|
||||
<svg viewBox="0 0 120 120" width="160" height="160">
|
||||
<circle cx="60" cy="60" r="52" fill="none" stroke="#e6f5f0" stroke-width="8" />
|
||||
<circle
|
||||
cx="60" cy="60" r="52"
|
||||
fill="none" stroke="#0f766e" stroke-width="8"
|
||||
stroke-dasharray={2 * Math.PI * 52}
|
||||
stroke-dashoffset={2 * Math.PI * 52 * (1 - count / target)}
|
||||
stroke-linecap="round"
|
||||
transform="rotate(-90 60 60)"
|
||||
/>
|
||||
<text x="60" y="56" text-anchor="middle" fill="#0f766e" font-size="22" font-weight="700">{count}</text>
|
||||
<text x="60" y="76" text-anchor="middle" fill="#5b8c85" font-size="10">/ {target}</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="total-line">
|
||||
<span>Total today:</span>
|
||||
<strong>{total}</strong>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="target-select">
|
||||
<label>Beads:</label>
|
||||
<select bind:value={target} onchange={() => { count = 0; }}>
|
||||
<option value={33}>33</option>
|
||||
<option value={99}>99</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={500}>500</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="primary" onclick={reset}>Reset</button>
|
||||
</div>
|
||||
|
||||
<p class="hint">👆 Tap the circle or press Space to count</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tasbih-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.counter-ring {
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
.counter-ring:active { transform: scale(0.97); transition: transform 0.1s; }
|
||||
.total-line { font-size: 0.9rem; color: #5b8c85; }
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.target-select {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: #5b8c85;
|
||||
}
|
||||
.target-select select {
|
||||
border: 1px solid #99d6c9;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
color: #0f766e;
|
||||
}
|
||||
.hint { font-size: 0.7rem; color: #94a3a8; }
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import App from './App.svelte';
|
||||
import { mount } from 'svelte';
|
||||
const app = mount(App, { target: document.getElementById('app') });
|
||||
export default app;
|
||||
Reference in New Issue
Block a user