Nūr Phase 1: Prayer, Qibla, Quran, Hijri, Tasbih, 99 Names — Svelte PWA
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<!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 />
|
||||||
|
<link rel="preconnect" href="https://api.aladhan.com" crossorigin />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
location /sw.js {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
add_header Service-Worker-Allowed "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+5833
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "muslimpro-clone-phase1",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
|
"svelte": "^5.20.0",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vite-plugin-pwa": "^0.21.0",
|
||||||
|
"workbox-precaching": "^7.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#0f766e"/>
|
||||||
|
<stop offset="100%" style="stop-color:#0d9488"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="100" height="100" rx="20" fill="url(#g)"/>
|
||||||
|
<text x="50" y="68" text-anchor="middle" fill="white" font-family="Georgia,serif" font-size="52" font-weight="bold">ن</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
+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,106 @@
|
|||||||
|
<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">
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
|
<div class="counter-ring" onclick={increment} role="button" tabindex="0" onkeydown={(e) => e.key === 'Enter' && 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 for="bead-select">Beads:</label>
|
||||||
|
<select id="bead-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;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
export default { preprocess: vitePreprocess() };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
svelte(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
manifest: {
|
||||||
|
name: 'Nūr — Muslim Companion',
|
||||||
|
short_name: 'Nūr',
|
||||||
|
description: 'Prayer times, Quran, Qibla, and more — your daily Islamic companion',
|
||||||
|
theme_color: '#0f766e',
|
||||||
|
background_color: '#f0fdfa',
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait-primary',
|
||||||
|
icons: [
|
||||||
|
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||||
|
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,svg,png,woff2,ico}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/api\.alquran\.cloud\/.*/i,
|
||||||
|
handler: 'StaleWhileRevalidate',
|
||||||
|
options: { cacheName: 'quran-api', expiration: { maxEntries: 50, maxAgeSeconds: 86400 * 7 } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user