Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9f02917
feat: add ConfigurationType.d.ts and defaults so we can strongly type…
mahfuzhannan Dec 24, 2025
eb023b0
fix: add new spinner component
mahfuzhannan Dec 25, 2025
53cd937
feat: add new today prayer times widget for iframe and allow the widg…
mahfuzhannan Dec 25, 2025
6205732
docs: update readme with new screenshots for admin page, announcement…
mahfuzhannan Dec 26, 2025
fdec321
fix: set prayer_time_tomorrow.enabled to true by default
mahfuzhannan Dec 29, 2025
7f0a3b9
fix: set car reg to empty string when announcement is general to over…
mahfuzhannan Dec 29, 2025
dc28c88
fix: google sheets read request per min is 300, better to set the ann…
mahfuzhannan Dec 29, 2025
c72493b
feat: update MosqueDataType.d.ts to support updated mosque api endpoi…
mahfuzhannan Dec 30, 2025
87a9a66
fix: sheetsGetConfigurationData was incorrectly merging sheets rows r…
mahfuzhannan Dec 30, 2025
e5cea63
fix: unify the image import in README.md
mahfuzhannan Dec 30, 2025
20791e2
fix: remove unnecessary logging
mahfuzhannan Dec 30, 2025
9ba7f62
fix: enforce en locale on moment dates and times as sometimes the dat…
mahfuzhannan Dec 30, 2025
b90a859
fix: incorrect format string for today widget
mahfuzhannan Dec 30, 2025
8999b49
fix: enforce en locale on moment dates and times as sometimes the dat…
mahfuzhannan Dec 30, 2025
2d59f59
feat: update to next 16 and add cache for google sheets service to li…
mahfuzhannan Dec 30, 2025
fe0b57e
fix: copy iframe full script instead of just the embed url
mahfuzhannan Dec 31, 2025
0a0ff95
fix: set default time for Clock, this was removed in previous commit
mahfuzhannan Dec 31, 2025
c611a9b
fix: remove nextjs cache components as this causes all pages to be dy…
mahfuzhannan Dec 31, 2025
f7dd13d
feat: migrate from moment usage across the app and use a datetime uti…
mahfuzhannan Dec 31, 2025
63c2b8a
fix: use "Asr" label instead of "'Asr" - keep consisten with app
mahfuzhannan Dec 31, 2025
08c0e64
fix: show tomorrow times when next prayer is not set, this is usually…
mahfuzhannan Dec 31, 2025
7510cdb
fix: google sheets does not contain month_label, we need to parse mon…
mahfuzhannan Dec 31, 2025
744dc93
fix: add day name to calendar, fix env example and datetime util miss…
mahfuzhannan Dec 31, 2025
2e774e9
fix: react19 and fix caching for GoogleSheetsService.ts
mahfuzhannan Jan 1, 2026
204cdc8
fix: use utils client enabled in announcement route to check if env v…
mahfuzhannan Jan 1, 2026
713eaa0
feat: migrate date manipulation to a single dateutil
mahfuzhannan Jan 1, 2026
ce1832c
feat: add datetimeUtils test cases and migrated more datetime formati…
mahfuzhannan Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ MOSQUE_API_ENDPOINT=http://localhost:3000/api/example-mosque
THEME_COLOR_HIGHLIGHT=#29B0EA
THEME_COLOR_PRIMARY=#003750
THEME_COLOR_PRIMARY_ALT=#086182
# Change local for date time in different locale
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

The comment contains a typo: "local" should be "locale".

🔎 Proposed fix
-# Change local for date time in different locale
+# Change locale for date time in different locale
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Change local for date time in different locale
# Change locale for date time in different locale
🤖 Prompt for AI Agents
.env.local.example around line 8: the comment reads "Change local for date time
in different locale" but contains a typo—replace "local" with "locale" so it
reads "Change locale for date time in different locale" (or better: "Change
locale for date/time"). Update the comment text accordingly.

LOCALE=en

# Admin Screen config
ADMIN_GOOGLE_SA_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n******\n-----END PRIVATE KEY-----\n"
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,30 @@ All of the code sits here:

### Mosque views

<img src="./public/demo-mosque-view-1.png" />
<img src="./public/screenshots/demo-mosque-view-1.png" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add alt text to images for accessibility.

All image tags are missing alt text, which impacts accessibility for screen reader users and provides better context when images fail to load.

🔎 Proposed fix with descriptive alt text
-<img src="./public/screenshots/demo-mosque-view-1.png" />
+<img src="./public/screenshots/demo-mosque-view-1.png" alt="Mosque prayer display screen showing today's prayer times" />

-<img src="./public/screenshots/demo-mosque-view-2.png" />
+<img src="./public/screenshots/demo-mosque-view-2.png" alt="Mosque prayer display screen showing upcoming prayer times" />

-<img src="./public/screenshots/demo-mosque-view-3.png" />
+<img src="./public/screenshots/demo-mosque-view-3.png" alt="Mosque prayer display screen with tomorrow prayer time hidden" />

-<img src="./public/screenshots/demo-mosque-announcement-car.png" />
+<img src="./public/screenshots/demo-mosque-announcement-car.png" alt="Mosque screen displaying car registration announcement" />

-<img src="./public/screenshots/demo-mosque-calendar-prints-1.png" />
+<img src="./public/screenshots/demo-mosque-calendar-prints-1.png" alt="Printable prayer calendar view 1" />
-<img src="./public/screenshots/demo-mosque-calendar-prints-2.png" />
+<img src="./public/screenshots/demo-mosque-calendar-prints-2.png" alt="Printable prayer calendar view 2" />

-<img src="./public/screenshots/demo-mobile-view.png" width="500px" />
+<img src="./public/screenshots/demo-mobile-view.png" width="500px" alt="Mobile progressive web app view of prayer times" />

-<img src="./public/screenshots/demo-admin-view.png/"/>
+<img src="./public/screenshots/demo-admin-view.png" alt="Admin interface for managing mosque screen settings" />

Also applies to: 49-49, 53-53, 57-57, 61-62, 66-66, 70-70

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

47-47: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In README.md around lines 47, 49, 53, 57, 61-62, 66, and 70, the <img> tags lack
alt attributes; update each <img> tag to include a concise, descriptive alt
attribute (e.g., "Screenshot: mosque view with prayer hall", "Screenshot: mosque
exterior at sunset", etc.), or use an empty alt (alt="") only for purely
decorative images, ensuring each image has appropriate accessibility-friendly
alt text that conveys its content or purpose.


<img src="./public/demo-mosque-view-2.png" />
<img src="./public/screenshots/demo-mosque-view-2.png" />

### Hide Tomorrow Prayer Time

<img src="./public/screenshots/demo-mosque-view-3.png" />

### Announcements

<img src="./public/screenshots/demo-mosque-announcement-car.png" />

### Print Calendars

<img src="./public/screenshots/demo-mosque-calendar-prints-1.png" />
<img src="./public/screenshots/demo-mosque-calendar-prints-2.png" />

### Mobile app

<img src="./public/demo-mobile-view.png" width="500px" />
<img src="./public/screenshots/demo-mobile-view.png" width="500px" />

### Admin Page

<img src="./public/screenshots/demo-admin-view.png/"/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix malformed image tag.

There's an extra / before the closing > which will break the image rendering.

🔎 Proposed fix
-<img src="./public/screenshots/demo-admin-view.png/"/>
+<img src="./public/screenshots/demo-admin-view.png" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img src="./public/screenshots/demo-admin-view.png/"/>
<img src="./public/screenshots/demo-admin-view.png" />
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

70-70: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In README.md around line 70, the image tag is malformed due to an extra '/'
before the closing '>' which prevents the image from rendering; remove the stray
'/' so the tag ends with '>' and ensure the src attribute points to the correct
relative path (./public/screenshots/demo-admin-view.png) and that the file
exists.


## How to get set up as a Mosque

Expand Down Expand Up @@ -121,7 +138,7 @@ The Admin interface will allow you to manage the screen from your phone these in
### Prerequisites

- Google Account
- Google Cloud Project (Optional for Admin interface)
- Google Cloud Project

### Step 1: Setup Google Cloud project

Expand Down Expand Up @@ -185,6 +202,7 @@ If you want to update your domain, you can do so by following the Vercel documen
| THEME_COLOR_ON_PRIMARY | #FFFFFF | #FFFFFF | The text color to be used when the background color is primary |
| THEME_COLOR_ON_PRIMARY_ALT | #FFFFFF | #FFFFFF | The text color to be used when the background color is primary alternative |
| THEME_COLOR_HIGHLIGHT | #10b981 | #10b981 | The color used to highlight upcoming prayer |
| LOCALE | en | en | The date/time format for locale |
| ADMIN_GOOGLE_SA_PRIVATE_KEY | "-----BEGIN PRIVATE KEY-----\n******\n-----END PRIVATE KEY-----\n" | | Required as part of Admin interface to interact with your google sheets |
| ADMIN_GOOGLE_SA_EMAIL | XXXX@XXXX-XXXX.iam.gserviceaccount.com | | Required as part of Admin interface to interact with your google sheets |
| AUTH_USERNAME | myuser | myuser | Required as part of Admin interface to login to admin page |
Expand Down
5 changes: 3 additions & 2 deletions app/api/data/announcements/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { sheetsGetAnnouncement, isSheetsClientReady } from '@/services/GoogleSheetsService';
import { sheetsGetAnnouncement } from '@/services/GoogleSheetsService';
import {
getAnnouncement,
} from '@/services/MosqueDataService'

import { type NextRequest } from 'next/server'
import { isSheetsClientEnabled } from "@/services/GoogleSheetsUtil"

export async function GET(request: NextRequest,) {
try {
let announcement = null;
// call the sheets API if we are connected, saves having to call external API
if (await isSheetsClientReady()) {
if (isSheetsClientEnabled()) {
announcement = await sheetsGetAnnouncement()
} else {
announcement = await getAnnouncement()
Expand Down
60 changes: 34 additions & 26 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getPrayerTimesForUpcomingDays,
getPrayerTimesForToday,
getPrayerTimesForTomorrow,
getConfiguration,
} from "@/services/MosqueDataService"
import type {
DailyPrayerTime,
Expand All @@ -24,6 +25,8 @@ import type { Metadata } from "next"
import UpcomingPrayerDayTiles from "@/components/UpcomingPrayerDayTiles/UpcomingPrayerDayTiles"
import "./prayer-times.css"
import Announcement from "@/components/Announcement/Announcement"
import { ConfigurationJson } from "@/types/ConfigurationType"
import { ConfigurationProvider } from "@/providers/ConfigurationProvider"

export async function generateMetadata(): Promise<Metadata> {
const mosqueMetadata: MosqueMetadataType = await getMetaData()
Expand All @@ -39,6 +42,7 @@ export default async function Home() {
const tomorrow: DailyPrayerTime = await getPrayerTimesForTomorrow()
const jummahTimes: JummahTimes = await getJummahTimes()
const mosqueMetadata: MosqueMetadataType = await getMetaData()
const config: ConfigurationJson = await getConfiguration()
const upcomingPrayerDays: UpcomingPrayerTimes[] =
await getPrayerTimesForUpcomingDays()

Expand All @@ -51,38 +55,42 @@ export default async function Home() {
]

upcomingPrayerDays.forEach((times) => {
slides.push(<UpcomingPrayerDayTiles times={times} key={times.display_date} />)
slides.push(
<UpcomingPrayerDayTiles times={times} key={times.display_date} />,
)
})

return (
<div className="bg-mosqueBrand min-h-screen min-w-full">
<main className="md:p-5">
<div className="md:grid md:grid-cols-8">
<div className="md:col-span-3">
<div className="p-4 md:p-6">
<Clock />
<ConfigurationProvider config={config}>
<div className="bg-mosqueBrand min-h-screen min-w-full">
<main className="md:p-5">
<div className="md:grid md:grid-cols-8">
<div className="md:col-span-3">
<div className="p-4 md:p-6">
<Clock />
</div>
<div className="p-4 md:p-6">
<Date />
</div>
<div className="p-4 md:p-6">
<MosqueMetadata metadata={mosqueMetadata} />
</div>
<div className="hidden md:p-6 md:block">
<Notice />
</div>
</div>
<div className="p-4 md:p-6">
<Date />
</div>
<div className="p-4 md:p-6">
<MosqueMetadata metadata={mosqueMetadata} />
</div>
<div className="hidden md:p-6 md:block">
<Notice />
<div className="p-4 md:p-6 md:col-span-5">
<PrayerTimes today={today} tomorrow={tomorrow} />
</div>
</div>
<div className="p-4 md:p-6 md:col-span-5">
<PrayerTimes today={today} tomorrow={tomorrow} />
<div className="p-4 md:p-6">
<SlidingBanner slides={slides} />
</div>
</div>
<div className="p-4 md:p-6">
<SlidingBanner slides={slides} />
</div>
<ServiceWorker />
</main>
<Announcement />
<Blackout prayerTimeToday={today} />
</div>
<ServiceWorker />
</main>
{config.feature.announcement.enabled && <Announcement />}
<Blackout prayerTimeToday={today} />
</div>
</ConfigurationProvider>
)
}
180 changes: 180 additions & 0 deletions app/widget/embed/today-prayer-times/TodayPrayerTime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { DailyPrayerTime } from "@/types/DailyPrayerTimeType"
import "../../widget.css"
import { getNextPrayer } from "@/services/PrayerTimeService"
import {
getPrayerTimesForToday,
getPrayerTimesForTomorrow,
} from "@/services/MosqueDataService"
import { cn } from "@/lib/utils"
import {
dtHijriNow,
dtNowFormatFull,
dtNowHijriFormatFull,
dtNowLocale,
dtFormatTimeToCustom,
} from "@/lib/datetimeUtils"

type Props = {
timeFormat?: "h:mm" | "h:mm A" | "HH:mm"
showSunrise?: boolean
showDate?: boolean
showHijri?: boolean
}

export async function TodayPrayerTime({
timeFormat = "h:mm",
showSunrise = false,
showDate = false,
showHijri = false,
}: Props) {
const convertTime = (time: string) =>
dtFormatTimeToCustom(time, timeFormat)

const today: DailyPrayerTime = await getPrayerTimesForToday()
const tomorrow: DailyPrayerTime = await getPrayerTimesForTomorrow()
let currentDailyPrayerTimes = today
let englishDate = dtNowFormatFull()
let hijriDate = dtNowHijriFormatFull()

let nextPrayerTime = getNextPrayer(today)
if (!nextPrayerTime.today) {
nextPrayerTime = {
today: true,
prayerIndex: 0
}
currentDailyPrayerTimes = tomorrow
englishDate = dtNowLocale().add(1, "day").format("D MMMM YYYY")
hijriDate = dtHijriNow().add(1, "day").format("iD iMMMM iYYYY")
}

let currentSalahTimes: Array<{
label: string
start: string
congregation: string | null
prayerIndex: number
}> = [
{
label: "Fajr",
start: currentDailyPrayerTimes.fajr.start,
congregation: currentDailyPrayerTimes.fajr.congregation_start,
prayerIndex: 0,
},
{
label: "Zuhr",
start: currentDailyPrayerTimes.zuhr.start,
congregation: currentDailyPrayerTimes.zuhr.congregation_start,
prayerIndex: 1,
},
{
label: "Asr",
start: currentDailyPrayerTimes.asr.start,
congregation: currentDailyPrayerTimes.asr.congregation_start,
prayerIndex: 2,
},
{
label: "Maghrib",
start: currentDailyPrayerTimes.maghrib.start,
congregation: currentDailyPrayerTimes.maghrib.congregation_start,
prayerIndex: 3,
},
{
label: "Isha",
start: currentDailyPrayerTimes.isha.start,
congregation: currentDailyPrayerTimes.isha.congregation_start,
prayerIndex: 4,
},
]

if (showSunrise) {
currentSalahTimes = [
...currentSalahTimes.slice(0, 1),
{
label: "Sunrise",
start: currentDailyPrayerTimes.sunrise_start,
congregation: null,
prayerIndex: -1,
},
...currentSalahTimes.slice(1),
]
}

return (
<div className="PrayerTimeWidgetWrapper">
<table className="w-full ml-0 text-center">
<thead>
{(showDate || showHijri) && (
<tr>
<th className={"pr-6 text-right text-gray-300"}></th>
<th
colSpan={currentSalahTimes?.length}
className={"text-gray-400 font-normal text-center text-md"}
>
{[showDate && englishDate, showHijri && hijriDate]
.filter(Boolean)
.join(" • ")}
</th>
</tr>
)}
<tr>
<th />
{currentSalahTimes.map((value, index) => (
<th
key={index}
className={cn(
"min-w-[10px] w-24",
nextPrayerTime.today &&
nextPrayerTime.prayerIndex === value.prayerIndex
? "bg-mosqueBrand-primaryAlt text-mosqueBrand-onPrimary rounded-t-md"
: "",
)}
>
{value.label}
</th>
))}
</tr>
</thead>
<tbody>
<tr>
<th className={"text-right text-gray-400 font-medium pr-2"}>
Begins
</th>
{currentSalahTimes.map((value, index) => (
<td
key={index}
className={cn(
"min-w-[10px] w-24",
nextPrayerTime.today &&
nextPrayerTime.prayerIndex === value.prayerIndex
? "bg-mosqueBrand-primaryAlt text-mosqueBrand-onPrimary"
: "",
)}
>
{value.start ? convertTime(value.start) : ""}
</td>
))}
</tr>
<tr>
<th className={"text-right text-gray-400 font-medium pr-2"}>
Jama&apos;ah
</th>

{currentSalahTimes.map((value, index) => (
<td
key={index}
className={cn(
"min-w-[10px] w-24",
nextPrayerTime.today &&
nextPrayerTime.prayerIndex === value.prayerIndex
? "bg-mosqueBrand-primaryAlt text-mosqueBrand-onPrimary rounded-b-md"
: "",
)}
>
{value.congregation ? convertTime(value.congregation) : ""}
</td>
))}
</tr>
</tbody>
</table>
</div>
)
}
Loading