Skip to content

Conversation

@mahfuzhannan
Copy link
Contributor

@mahfuzhannan mahfuzhannan commented Nov 20, 2025

This change adds a new Admin Page to the app, which currently as of this PR allows showing an announcement on the screen.

Admin Page requires username/password authentication which is defined as ENV vars to keep the setup simple. Google Cloud project service account needs to be setup for the backend to be able to edit the sheet data.

Non breaking change - existing screens are not impacted.

Summary by CodeRabbit

  • New Features

    • Admin interface with secure sign-in, session gating, dashboard, and announcement management; announcement UI/modal and vehicle-specific notices.
    • Google Sheets integration for storing/retrieving announcements and configuration.
    • New UI primitives: modal, loading spinner, primary button, announcement components.
  • Documentation

    • Expanded README with Admin Interface setup, env examples, and deployment steps.
  • Tests

    • Added unit tests and Jest configuration.
  • Chores

    • Added environment config keys (auth, service account, spreadsheet, theme colors), ignored IDE folder, and tightened service-worker GET handling.

✏️ Tip: You can customize this high-level summary in your review settings.

…cements.

- added admin page with username/password authentication which is set by ENV vars
- integrated google sheets api, requiring google credentials to be used to update the sheets (client side oAuth was reverted as it required google cloud projects to be verified)
…ble or not, we'll use the username/password and the google service account creds.

- add some error handling to the announcement route so we can show the error in the UI
- formatting changes
@vercel
Copy link

vercel bot commented Nov 20, 2025

@mahfuzhannan is attempting to deploy a commit to the MosqueOS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds an admin interface with NextAuth (credentials/Google), announcement creation/display UIs, Google Sheets-backed storage and API routes, supporting utility functions, UI primitives, new types, environment variables, test/jest configuration, and related docs/config updates.

Changes

Cohort / File(s) Summary
Authentication & Session
app/auth.ts, app/api/auth/[...nextauth]/route.ts, app/admin/SessionProviderWrapper.tsx
Adds NextAuth configuration (Google + Credentials conditional), exports authOptions, getSession, isAdminInterfaceEnabled; exposes NextAuth route handler (GET/POST); adds client SessionProvider wrapper.
Admin Page & Server Flow
app/admin/page.tsx
New server admin page that gates on isAdminInterfaceEnabled, checks session (redirects to sign-in if missing), fetches metadata, and renders AdminPage within session wrapper. Exports page metadata constant.
Admin UI Components
components/Admin/AdminPage.tsx, components/Admin/Announcement/AddAnnouncement.tsx, components/Admin/Announcement/AnnoumentForm.tsx
Adds AdminPage, AddAnnouncement (fetches current announcement, shows status, opens modal), and AnnouncementForm (form state, time calculations, submits via createAnnouncement).
Public Announcement UI
components/Announcement/Announcement.tsx, components/Announcement/AnnouncementModal.tsx
Adds Announcement (polls /api/data/announcements every 10s, shows modal when visible) and AnnouncementModal (centered overlay).
Data Services & API
services/GoogleSheetsService.ts, services/MosqueDataService.ts, app/api/data/announcements/route.ts
New Google Sheets integration using service account JWT; helpers to read/update configuration and announcements; MosqueDataService adds getAnnouncement and createAnnouncement; API route selects Sheets or fallback data source.
Sheets Utilities & Tests
services/GoogleSheetsUtil.ts, services/GoogleSheetsUtil.test.ts
New utilities to flatten/nest JSON ↔ sheet rows and tests validating flatten/nest behavior.
UI Primitives
components/ui/Modal.tsx, components/ui/LoadingSpinner.tsx, components/ui/Buttons.tsx
Adds Modal (portal, Escape-to-close, sizes), LoadingSpinner, and PrimaryButton components.
Types & Declarations
types/AnnouncementType.d.ts, types/MosqueDataType.d.ts
Adds AnnouncementData interface and updates mosque-related type aliases and adds configuration? field.
Config, Build & Tests
package.json, jest.config.js, tsconfig.json, tsconfig.json
Adds runtime deps googleapis, next-auth; adds Jest config and test/dev dependencies; updates TS target/libs and includes.
Docs & Examples
README.md, .env.local.example, .gitignore
README expanded with Admin Interface setup and env examples; .env.local.example adds admin/Google/stylesheet/theme/auth vars; .gitignore ignores /.idea.
Misc & Runtime
public/service-worker.js, app/page.tsx, app/layout.tsx
Service worker early-returns non-GETs; main page renders Announcement and fixes list keys; metadata template placeholder adjusted.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant NextServer
    participant SessionStore
    participant MosqueDataService
    participant GoogleSheets

    User->>Browser: Navigate to /admin
    Browser->>NextServer: GET /admin
    NextServer->>SessionStore: getSession(authOptions)
    alt No session
        NextServer->>Browser: Redirect to /api/auth/signin
    else Session exists
        NextServer->>MosqueDataService: getMetaData()
        MosqueDataService->>GoogleSheets: sheetsReadConfiguration()
        GoogleSheets-->>MosqueDataService: configuration
        NextServer->>Browser: Render AdminPage (SessionProviderWrapper)
    end
Loading
sequenceDiagram
    actor Admin
    participant Browser
    participant AddAnnouncement
    participant Modal
    participant AnnouncementForm
    participant MosqueDataService
    participant GoogleSheets
    participant APIRoute

    Admin->>Browser: Click "Add Announcement"
    Browser->>AddAnnouncement: open modal
    AddAnnouncement->>Modal: render AnnouncementForm
    Admin->>AnnouncementForm: submit(formData)
    AnnouncementForm->>MosqueDataService: createAnnouncement(formData)
    MosqueDataService->>GoogleSheets: sheetsUpdateConfiguration()
    GoogleSheets-->>MosqueDataService: success
    MosqueDataService-->>AnnouncementForm: resolved
    AnnouncementForm->>AddAnnouncement: onComplete(updated)
    AddAnnouncement->>Modal: close()
    APIRoute->>MosqueDataService: GET returns updated announcement
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus areas:
    • services/GoogleSheetsService.ts: service account JWT auth, session checks, Sheets API transformations.
    • app/auth.ts and app/api/auth/[...nextauth]/route.ts: provider configuration and credential auth logic.
    • services/GoogleSheetsUtil.ts and tests: correctness of flatten/nest conversions.
    • components/Admin/Announcement/* and components/Announcement/*: client polling, modal/form state, time calculations.
    • Modal SSR/portal logic and accessibility.

Poem

🐰 I hopped into code beneath the moonlight,
Sheets and sessions stitched the admin bright,
Modals popped open with a cheerful squeak,
Announcements whispered row by row each week,
A tiny rabbit nods: "Deployed — delight!"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.34% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feature: Admin Page and Announcements' accurately summarizes the main changes: adding an admin interface and announcement functionality.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (14)
types/AnnouncementType.d.ts (1)

1-10: Consider documenting why duration_minutes is a string.

The field name suggests a numeric value, but it's typed as string | null. If this is intentional (e.g., for Google Sheets compatibility), consider adding a comment to clarify the design decision.

.env.local.example (2)

14-16: Provide guidance for generating a secure AUTH_SECRET.

The example value "some-long-random-string" is insufficient. NextAuth requires a strong, cryptographically random secret for security. Add a comment explaining how to generate a secure value.

Apply this diff to add guidance:

+# Generate a secure secret with: openssl rand -base64 32
 AUTH_USERNAME=myuser
 AUTH_PASSWORD=secret
 AUTH_SECRET=some-long-random-string

11-11: Consider addressing static analysis suggestions (optional).

Static analysis flagged minor style issues:

  • Key ordering: place ADMIN_GOOGLE_SA_EMAIL before ADMIN_GOOGLE_SA_PRIVATE_KEY, and AUTH_PASSWORD and AUTH_SECRET before AUTH_USERNAME
  • Add a blank line at the end of the file

These are optional improvements for consistency.

Also applies to: 15-16

README.md (1)

146-146: Add language identifiers to code blocks.

The fenced code blocks on lines 146 and 155 should specify a language for better rendering and syntax highlighting.

Add bash or env language identifiers:

-```
+```bash
 e.g.
-```
+```env
 AUTH_USERNAME=myuser 
 AUTH_PASSWORD=secret


Also applies to: 155-155

</blockquote></details>
<details>
<summary>app/api/data/announcements/route.ts (3)</summary><blockquote>

`7-7`: **Unused parameter.**

The `request: NextRequest` parameter is declared but never used in the handler.



If you don't need to read query parameters or headers, remove it:

```diff
-export async function GET(request: NextRequest,) {
+export async function GET() {

Or if cache busting is important, you could verify the no_cache param is present for debugging purposes (though this is optional).


7-7: Remove trailing comma.

There's an unnecessary trailing comma after the NextRequest parameter.

-export async function GET(request: NextRequest,) {
+export async function GET(request: NextRequest) {

11-12: Consider more specific error status codes.

Returning 400 (Bad Request) for all errors is misleading. A 400 status indicates a client error, but failures in getAnnouncement() (like Google Sheets API errors) are server errors and should return 500.

   } catch (error: any) {
-    return Response.json({ error: error?.message ?? "Unknown error" }, { status: 400 });
+    return Response.json({ error: error?.message ?? "Unknown error" }, { status: 500 });
   }
app/admin/SessionProviderWrapper.tsx (1)

6-8: Complete the documentation comment.

The comment is incomplete: "So that we don't have to wrap the whole app in a session provider and keep" - the sentence trails off.

Complete the thought or simplify:

 /**
- * So that we don't have to wrap the whole app in a session provider and keep
- * @param children
- * @constructor
+ * Wraps children with NextAuth SessionProvider for admin pages only.
+ * This avoids wrapping the entire app when session is only needed in /admin.
  */
components/Announcement/Announcement.tsx (1)

11-24: Optionally trigger an immediate fetch before starting the polling interval

Right now the first announcement won’t be fetched until 10 seconds after mount. If you want announcements to appear as soon as the page loads, consider doing one fetch call before setInterval (or calling the polling function once before registering the interval).

app/admin/page.tsx (1)

9-60: Admin gating and metadata usage look correct; consider dropping unnecessary async

The feature-flag + session check flow in AdminPageWrapper and the metadata generation both look good for the admin route.

You can optionally remove async from AdminServerPage since it doesn’t await anything; that will slightly simplify typing and make intent clearer.

components/Admin/Announcement/AnnoucementForm.tsx (1)

25-36: Align default start time with the “+2 minutes” comment

The comment says “Start time = now + 2 minutes”, but start is just a clone of now with no offset. If you really want a 2‑minute offset, adjust the timestamp:

-    // Start time = now + 2 minutes
-    const start = new Date(now.getTime())
+    // Start time = now + 2 minutes
+    const start = new Date(now.getTime() + 2 * 60 * 1000)
services/GoogleSheetsService.ts (3)

8-37: Validate required Google Sheets env vars up front for clearer failures

Right now SPREADSHEET_ID can be '' and the service account email/key can be undefined, which will only surface later as opaque Google API/auth errors. It’s better to fail fast with a clear message when configuration is missing.

You can keep the call sites unchanged and add a simple guard:

-const SPREADSHEET_ID = process.env.SPREADSHEET_ID ?? ''
-const ADMIN_GOOGLE_SA_PRIVATE_KEY = process.env.ADMIN_GOOGLE_SA_PRIVATE_KEY
-const ADMIN_GOOGLE_SA_EMAIL = process.env.ADMIN_GOOGLE_SA_EMAIL
+const SPREADSHEET_ID = process.env.SPREADSHEET_ID
+const ADMIN_GOOGLE_SA_PRIVATE_KEY = process.env.ADMIN_GOOGLE_SA_PRIVATE_KEY
+const ADMIN_GOOGLE_SA_EMAIL = process.env.ADMIN_GOOGLE_SA_EMAIL
@@
 export async function getUserSheetsClient () {
   const session = await getSession() // next-auth v5 app router helper
 
   if (!session) {
     throw new Error('Not authenticated with Google')
   }
 
+  if (!SPREADSHEET_ID || !ADMIN_GOOGLE_SA_EMAIL || !ADMIN_GOOGLE_SA_PRIVATE_KEY) {
+    throw new Error('Google Sheets service account env vars are not fully configured')
+  }
+
   try {
@@
-    const googleAuthJwt = new google.auth.JWT({
-      email: ADMIN_GOOGLE_SA_EMAIL,
-      key: ADMIN_GOOGLE_SA_PRIVATE_KEY?.replace(/\\n/g, '\n'),
+    const googleAuthJwt = new google.auth.JWT({
+      email: ADMIN_GOOGLE_SA_EMAIL,
+      key: ADMIN_GOOGLE_SA_PRIVATE_KEY.replace(/\\n/g, '\n'),
       scopes: ['https://www.googleapis.com/auth/spreadsheets'],
     })

39-51: Harden announcement JSON parsing against corrupt sheet data

sheetsGetAnnouncement assumes the metadata cell always contains valid JSON. A malformed value will cause JSON.parse to throw and break the admin flow.

Consider making parsing defensive so a bad cell just results in “no announcement” (or a logged warning) instead of a crash:

 export async function sheetsGetAnnouncement (): Promise<AnnouncementData | null> {
   const data = await sheetsGetMetadataValueByKey('announcement')
 
-  if (data) {
-    return JSON.parse(data) as AnnouncementData
-  }
-  return null
+  if (!data) return null
+
+  try {
+    return JSON.parse(data) as AnnouncementData
+  } catch (error) {
+    console.error('Failed to parse announcement JSON from Metadata sheet', error)
+    return null
+  }
 }

85-121: Metadata update helper error message should include the actual key

sheetsUpdateMetadataValueByKey throws if the key row is missing, but the error message always says it couldn’t find 'announcement', which is misleading when this helper is used for other metadata keys.

You can keep the behavior (throw on missing key) while making the message accurate:

   const targetIndex = rows.findIndex(
     (row) => row[0]?.trim() === key,
   )
 
   if (targetIndex === -1) {
-    throw new Error(
-      `Could not find 'announcement' row in sheet ${SHEET_NAME_METADATA}`)
+    throw new Error(
+      `Could not find '${key}' row in sheet ${SHEET_NAME_METADATA}`)
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 585838f and 9f6a441.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • .env.local.example (1 hunks)
  • .gitignore (1 hunks)
  • README.md (8 hunks)
  • app/admin/SessionProviderWrapper.tsx (1 hunks)
  • app/admin/page.tsx (1 hunks)
  • app/api/auth/[...nextauth]/route.ts (1 hunks)
  • app/api/data/announcements/route.ts (1 hunks)
  • app/auth.ts (1 hunks)
  • app/page.tsx (3 hunks)
  • components/Admin/AdminPage.tsx (1 hunks)
  • components/Admin/Announcement/AddAnnouncement.tsx (1 hunks)
  • components/Admin/Announcement/AnnoucementForm.tsx (1 hunks)
  • components/Announcement/AnnoucementModal.tsx (1 hunks)
  • components/Announcement/Announcement.tsx (1 hunks)
  • components/ui/Buttons.tsx (1 hunks)
  • components/ui/LoadingSpinner.tsx (1 hunks)
  • components/ui/Modal.tsx (1 hunks)
  • package.json (1 hunks)
  • public/service-worker.js (1 hunks)
  • services/GoogleSheetsService.ts (1 hunks)
  • services/MosqueDataService.ts (2 hunks)
  • tsconfig.json (2 hunks)
  • types/AnnouncementType.d.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
app/api/data/announcements/route.ts (1)
services/MosqueDataService.ts (1)
  • getAnnouncement (92-108)
app/admin/page.tsx (6)
app/page.tsx (1)
  • generateMetadata (28-35)
types/MosqueDataType.d.ts (1)
  • MosqueMetadataType (10-10)
services/MosqueDataService.ts (1)
  • getMetaData (86-90)
app/admin/SessionProviderWrapper.tsx (1)
  • SessionProviderWrapper (10-12)
app/auth.ts (3)
  • isAdminInterfaceEnabled (80-80)
  • getSession (80-80)
  • redirect (54-57)
components/Admin/AdminPage.tsx (1)
  • AdminPage (5-33)
components/Announcement/Announcement.tsx (2)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
components/Announcement/AnnoucementModal.tsx (1)
  • AnnouncementModal (8-30)
components/Admin/Announcement/AddAnnouncement.tsx (5)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
components/ui/LoadingSpinner.tsx (1)
  • LoadingSpinner (3-11)
components/ui/Buttons.tsx (1)
  • PrimaryButton (4-24)
components/ui/Modal.tsx (1)
  • Modal (21-91)
components/Admin/Announcement/AnnoucementForm.tsx (1)
  • AnnouncementForm (13-168)
components/Admin/Announcement/AnnoucementForm.tsx (3)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
services/MosqueDataService.ts (1)
  • createAnnouncement (110-112)
components/ui/LoadingSpinner.tsx (1)
  • LoadingSpinner (3-11)
components/Admin/AdminPage.tsx (2)
types/MosqueDataType.d.ts (1)
  • MosqueMetadataType (10-10)
components/Admin/Announcement/AddAnnouncement.tsx (1)
  • AddAnnouncement (10-81)
services/MosqueDataService.ts (2)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
services/GoogleSheetsService.ts (2)
  • sheetsGetAnnouncement (39-46)
  • sheetsUpdateAnnouncement (48-51)
services/GoogleSheetsService.ts (2)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
types/DailyPrayerTimeType.d.ts (1)
  • DailyPrayerTime (1-11)
app/api/auth/[...nextauth]/route.ts (1)
app/auth.ts (1)
  • authOptions (80-80)
app/page.tsx (2)
components/UpcomingPrayerDayTiles/UpcomingPrayerDayTiles.tsx (1)
  • UpcomingPrayerDayTiles (4-63)
components/Announcement/Announcement.tsx (1)
  • Announcement (7-70)
🪛 Biome (2.1.2)
components/Admin/Announcement/AddAnnouncement.tsx

[error] 10-11: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

🪛 dotenv-linter (4.0.0)
.env.local.example

[warning] 11-11: [UnorderedKey] The ADMIN_GOOGLE_SA_EMAIL key should go before the ADMIN_GOOGLE_SA_PRIVATE_KEY key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The AUTH_PASSWORD key should go before the AUTH_USERNAME key

(UnorderedKey)


[warning] 16-16: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 16-16: [UnorderedKey] The AUTH_SECRET key should go before the AUTH_USERNAME key

(UnorderedKey)

🪛 LanguageTool
README.md

[style] ~99-~99: Consider using a different verb to strengthen your wording.
Context: ...n generate your own endpoint but please make sure it has all the required properties. ##...

(MAKE_SURE_ENSURE)


[uncategorized] ~189-~189: “Google” is a proper noun and needs to be capitalized.
Context: ...f Admin interface to interact with your google sheets | | ADMIN_GOOGLE_SA_EMAI...

(A_GOOGLE)


[uncategorized] ~190-~190: “Google” is a proper noun and needs to be capitalized.
Context: ...f Admin interface to interact with your google sheets | | AUTH_USERNAME ...

(A_GOOGLE)

🪛 markdownlint-cli2 (0.18.1)
README.md

22-22: Link text should be descriptive

(MD059, descriptive-link-text)


129-129: Bare URL used

(MD034, no-bare-urls)


134-134: Bare URL used

(MD034, no-bare-urls)


146-146: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


155-155: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


179-179: Bare URL used

(MD034, no-bare-urls)


190-190: Bare URL used

(MD034, no-bare-urls)


214-214: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (19)
public/service-worker.js (1)

30-33: LGTM! This follows service worker best practices.

Restricting the service worker to only handle GET requests is correct. Non-GET requests (POST, PUT, DELETE) should not be cached since they are not idempotent and may have side effects. This is particularly important for the admin functionality being added in this PR, where authentication and announcement creation will use POST requests.

.gitignore (1)

21-21: LGTM!

Adding .idea to .gitignore is appropriate for excluding JetBrains IDE configuration files from version control.

components/ui/LoadingSpinner.tsx (1)

1-11: LGTM!

The LoadingSpinner component is clean and straightforward. The implementation correctly uses Tailwind CSS for styling and animations.

tsconfig.json (1)

31-40: LGTM!

The TypeScript configuration updates are appropriate. Adding .next/dev/types/**/*.ts to the include patterns aligns with Next.js 15 development type generation, and the formatting changes improve readability.

package.json (1)

19-19: No active security vulnerabilities detected for the added versions.

Verification shows googleapis@^105.0.0 and next-auth@^4.24.13 have no known security advisories:

  • googleapis 105.0.0 is not affected by the documented HIGH severity authorization vulnerability (affects only < 39.1.0)
  • next-auth 4.24.13 is not affected by any documented advisories, including the recent MODERATE email misdelivery vulnerability (affects < 4.24.12) and the HIGH OAuth vulnerability (affects < 4.20.1)

While newer versions are available, the specified versions are stable and secure for use.

README.md (1)

60-60: LGTM! Helpful clarification.

Good addition to clarify that Google Cloud Project is optional and only needed for the admin interface.

components/ui/Modal.tsx (3)

6-19: LGTM! Well-typed modal props.

The type definitions are clear and complete, with sensible defaults.


30-39: LGTM! Proper keyboard event handling.

The ESC key handler is correctly implemented with proper cleanup.


49-49: Backdrop click is disabled.

The backdrop onClick={onClose} is commented out, meaning users cannot close the modal by clicking outside it. This is a valid design choice but differs from common modal UX patterns.

Confirm this is intentional. If users should be able to close by clicking the backdrop, uncomment line 49.

app/page.tsx (2)

53-55: LGTM! Proper React key added.

Adding key={times.display_date} correctly satisfies React's requirement for list items and uses an appropriate unique identifier.


84-84: LGTM! Announcement component integrated.

The component is correctly placed at the page level and will manage its own state and fetching logic.

components/Admin/AdminPage.tsx (2)

1-9: LGTM! Component structure is clear.

The component signature and imports are well-organized.


11-33: LGTM! Clean admin page layout.

The page structure is well-organized with a branded header and content area. The responsive design uses appropriate Tailwind breakpoints.

app/api/auth/[...nextauth]/route.ts (1)

1-6: LGTM! Standard NextAuth route setup.

This follows the recommended pattern for NextAuth in Next.js App Router, correctly exporting both GET and POST handlers.

app/auth.ts (3)

14-28: Google OAuth configured with forced consent.

The configuration is correct, but note that prompt: "consent" will show the consent screen on every login, which may impact UX.

If this is intentional (to ensure you always get a refresh token), this is fine. Otherwise, consider prompt: "select_account" which only prompts when necessary:

         params: {
           access_type: "offline",
-          prompt: "consent",
+          prompt: "select_account",
           scope: "openid email profile https://www.googleapis.com/auth/spreadsheets"

51-63: LGTM! Auth configuration is appropriate.

The redirect behavior and theme configuration are correctly implemented.


65-79: LGTM! Helpful utility functions.

Both helper functions are well-implemented. The isAdminInterfaceEnabled logic correctly checks for either authentication method.

components/Admin/Announcement/AddAnnouncement.tsx (1)

36-77: Announcement card + modal wiring looks coherent

The state flow from initial fetch → currentAnnouncement → details view, plus opening the modal and updating currentAnnouncement via onComplete, is consistent and easy to follow.

services/MosqueDataService.ts (1)

92-112: Announcement visibility calculation looks good; be aware of server timezone

The getAnnouncement helper cleanly centralizes the is_visible computation based on date, start_time, and end_time, and returns null when no date is set, which matches how the UI handles “no announcement”.

Just note that moment() here uses the server’s local timezone; if you ever deploy across different timezones or move to UTC‑based data, you may want to standardize this to avoid surprises.

Comment on lines +1 to +30
import React from 'react'

type AnnouncementModalProps = {
isOpen: boolean;
children: React.ReactNode;
};

export default function AnnouncementModal({
isOpen,
children,
}: AnnouncementModalProps) {
if (!isOpen) return null;

return (
<div
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm"
aria-modal="true"
role="dialog"
>
<div
className={`w-full max-w-6xl mx-4 rounded-2xl bg-white shadow-xl`}
onClick={(e) => e.stopPropagation()} // prevent close when clicking inside
>
<div className="px-2 py-4 text-sm text-slate-700">
{children}
</div>
</div>
</div>
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Filename typo: "Annoucement" should be "Announcement".

The filename is AnnoucementModal.tsx (missing an 'n'), while the component is correctly named AnnouncementModal. This inconsistency can cause confusion and import issues.

Rename the file to AnnouncementModal.tsx and verify all imports are updated:


🏁 Script executed:

#!/bin/bash
# Search for imports of the misspelled filename
rg -n "AnnoucementModal" --type ts --type tsx

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for imports of the misspelled filename - corrected syntax
rg -n "AnnoucementModal" --type-list | grep -E "(ts|tsx)" || rg -n "AnnoucementModal" -g "*.ts" -g "*.tsx"

Length of output: 511


🏁 Script executed:

#!/bin/bash
# Search for all references to the misspelled filename
rg -n "AnnoucementModal" -g "*.ts" -g "*.tsx"

Length of output: 121


Rename AnnoucementModal.tsx to AnnouncementModal.tsx and update the import in git/components/Announcement/Announcement.tsx line 5.

The filename contains a typo (missing 'n') while the component is correctly named AnnouncementModal. The misspelled filename is currently imported in git/components/Announcement/Announcement.tsx.

🤖 Prompt for AI Agents
In components/Announcement/AnnoucementModal.tsx around lines 1 to 30, the
filename is misspelled (AnnoucementModal.tsx) while the component is correctly
named AnnouncementModal; rename the file to
components/Announcement/AnnouncementModal.tsx and update the import statement on
git/components/Announcement/Announcement.tsx at line 5 to import from
"./AnnouncementModal" (adjust path if needed), then run a quick project-wide
search to update any other imports and commit the rename so exports and import
paths remain consistent.

Comment on lines 53 to 83
async function sheetsGetMetadataValueByKey (key: string): Promise<string | null> {
try {
const sheets = await getUserSheetsClient()
const metadataValueRange = await sheets.spreadsheets.values.get({
spreadsheetId: SPREADSHEET_ID,
range: SHEET_NAME_METADATA, // or your sheet name
})
const rows = metadataValueRange.data.values || []
const targetIndex = rows.findIndex(
(row) => row[0]?.trim() === key,
)

if (targetIndex === -1) {
throw new Error(
`Could not find 'announcement' row in sheet ${SHEET_NAME_METADATA}`)
}

// The value is in column B = column index 1
const cell = `${SHEET_NAME_METADATA}!B${targetIndex + 1}` // add 1 because Google Sheets is 1-indexed

const data = await sheets.spreadsheets.values.get({
spreadsheetId: SPREADSHEET_ID,
range: cell,
})

return data?.data?.values?.[0]?.[0] ?? null
} catch (error: any) {
console.error(error)
throw new Error(`Google Sheets API request failed: ${error?.message}`)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Behavior of sheetsGetMetadataValueByKey doesn’t match its signature and hardcodes 'announcement'

Two issues here:

  1. The function is typed to return Promise<string | null>, but a missing key causes it to throw instead of returning null. That means sheetsGetAnnouncement can never see a “no data” state; it only gets a value or an exception.
  2. The error message hardcodes 'announcement', even though key can be anything.

If you want “null if key missing” semantics (which aligns with the return type and announcement use case), you can change the missing-row branch to return null and fix the message:

   const rows = metadataValueRange.data.values || []
   const targetIndex = rows.findIndex(
     (row) => row[0]?.trim() === key,
   )
 
   if (targetIndex === -1) {
-      throw new Error(
-        `Could not find 'announcement' row in sheet ${SHEET_NAME_METADATA}`)
+    // Key not present in Metadata sheet
+    return null
   }
@@
   } catch (error: any) {
     console.error(error)
-    throw new Error(`Google Sheets API request failed: ${error?.message}`)
+    throw new Error(`Google Sheets API request failed for key '${key}': ${error?.message}`)
   }

If you do intend absence to be exceptional, I’d recommend updating the return type to Promise<string> and updating the error message to interpolate key instead of 'announcement'.

📝 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
async function sheetsGetMetadataValueByKey (key: string): Promise<string | null> {
try {
const sheets = await getUserSheetsClient()
const metadataValueRange = await sheets.spreadsheets.values.get({
spreadsheetId: SPREADSHEET_ID,
range: SHEET_NAME_METADATA, // or your sheet name
})
const rows = metadataValueRange.data.values || []
const targetIndex = rows.findIndex(
(row) => row[0]?.trim() === key,
)
if (targetIndex === -1) {
throw new Error(
`Could not find 'announcement' row in sheet ${SHEET_NAME_METADATA}`)
}
// The value is in column B = column index 1
const cell = `${SHEET_NAME_METADATA}!B${targetIndex + 1}` // add 1 because Google Sheets is 1-indexed
const data = await sheets.spreadsheets.values.get({
spreadsheetId: SPREADSHEET_ID,
range: cell,
})
return data?.data?.values?.[0]?.[0] ?? null
} catch (error: any) {
console.error(error)
throw new Error(`Google Sheets API request failed: ${error?.message}`)
}
}
async function sheetsGetMetadataValueByKey (key: string): Promise<string | null> {
try {
const sheets = await getUserSheetsClient()
const metadataValueRange = await sheets.spreadsheets.values.get({
spreadsheetId: SPREADSHEET_ID,
range: SHEET_NAME_METADATA, // or your sheet name
})
const rows = metadataValueRange.data.values || []
const targetIndex = rows.findIndex(
(row) => row[0]?.trim() === key,
)
if (targetIndex === -1) {
// Key not present in Metadata sheet
return null
}
// The value is in column B = column index 1
const cell = `${SHEET_NAME_METADATA}!B${targetIndex + 1}` // add 1 because Google Sheets is 1-indexed
const data = await sheets.spreadsheets.values.get({
spreadsheetId: SPREADSHEET_ID,
range: cell,
})
return data?.data?.values?.[0]?.[0] ?? null
} catch (error: any) {
console.error(error)
throw new Error(`Google Sheets API request failed for key '${key}': ${error?.message}`)
}
}
🤖 Prompt for AI Agents
In services/GoogleSheetsService.ts around lines 53-83, the function currently
throws when the key row is not found and the error text hardcodes
'announcement'; update it to match the declared Promise<string | null> by
returning null when targetIndex === -1 (do not throw), and also remove or
replace the hardcoded 'announcement' string so any thrown error (if you keep
any) interpolates the provided key; keep the rest of the logic the same so a
missing key yields null to callers like sheetsGetAnnouncement.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
components/Admin/Announcement/AnnoucementForm.tsx (1)

38-65: Improve submit handler: fix typo, clear error state, and simplify with async/await

The current handler works but has a few rough edges:

  • The variable is misspelled as annoucenment, which makes the code harder to read.
  • error is never cleared on a new submit, so a previous error message will remain visible even after a successful retry.
  • The .then/.catch/.finally chain is a bit noisy given modern async/await.

You can address all three in one pass:

-  const handleSubmit = (e: React.FormEvent) => {
+  const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault()
 
-    const annoucenment: AnnouncementData = {
+    const announcement: AnnouncementData = {
       date,
       start_time: startTime,
       end_time: moment(`${date} ${startTime}`).
         add(duration, 'minutes').
         format('HH:mm'),
       message,
       car_reg_number: type === 'Car' ? carReg : null,
     }
 
-    console.log('Submitting:', annoucenment)
-    // Perform your API POST or upload here...
-
-    setIsLoading(true)
-    createAnnouncement(annoucenment).
-      then(() => {
-        console.log('Announcement added successfully')
-        onComplete(annoucenment)
-      }).
-      catch((error) => setError(`Error creating announcement: ${error}`)).
-      finally(() => {
-        setIsLoading(false)
-      })
+    console.log('Submitting:', announcement)
+
+    setError(null)
+    setIsLoading(true)
+    try {
+      await createAnnouncement(announcement)
+      onComplete(announcement)
+    } catch (error) {
+      setError(`Error creating announcement: ${error}`)
+    } finally {
+      setIsLoading(false)
+    }
   }

This ensures a clean error state on each submit, fixes the naming typo, and keeps the flow easier to follow.

services/GoogleSheetsService.ts (1)

53-83: Align sheetsGetMetadataValueByKey behavior with its Promise<string | null> return type

Right now:

  • The function is typed as Promise<string | null>, but if the key row is missing it throws instead of returning null.
  • Callers like sheetsGetAnnouncement therefore can’t distinguish “no announcement set yet” from a genuine API failure.

Returning null when the key is absent better matches the type and the intended “optional metadata” use case, while reserving throws for real API errors. For example:

   const rows = metadataValueRange.data.values || []
   const targetIndex = rows.findIndex(
     (row) => row[0]?.trim() === key,
   )
 
   if (targetIndex === -1) {
-      throw new Error(
-        `Could not find '${key}' row in sheet ${SHEET_NAME_METADATA}`)
+    // Key not present in Metadata sheet
+    return null
   }
@@
   } catch (error: any) {
     console.error(error)
-    throw new Error(`Google Sheets API request failed: ${error?.message}`)
+    throw new Error(`Google Sheets API request failed for key '${key}': ${error?.message}`)
   }

That way sheetsGetAnnouncement can safely treat null as “no announcement configured” and only surface hard errors when the Sheets API itself fails.

🧹 Nitpick comments (5)
components/Announcement/AnnouncementModal.tsx (1)

14-28: Optional: Consider reusing the shared Modal for consistency and a11y

This component largely duplicates the backdrop/layout logic from components/ui/Modal.tsx but without ESC handling, title wiring, or a close button. If the behavior is meant to be the same, consider reusing Modal (or extracting shared overlay styles) so modal behavior and accessibility stay in sync in one place.

components/Admin/Announcement/AddAnnouncement.tsx (1)

3-8: Unify “Announcement” spelling in file/import names

The form file and import path use AnnoucementForm (missing the second “n”) while the component itself is named AnnouncementForm. This inconsistency makes the component harder to find and is easy to mis-type later.

Consider renaming the file and updating this import:

-import { AnnouncementForm } from '@/components/Admin/Announcement/AnnoucementForm'
+import { AnnouncementForm } from '@/components/Admin/Announcement/AnnouncementForm'

…and then git mv components/Admin/Announcement/AnnoucementForm.tsx components/Admin/Announcement/AnnouncementForm.tsx plus any other imports that reference the old path.

README.md (2)

146-158: Add language identifiers to fenced env‑var code blocks (markdownlint MD040)

The two env‑var example blocks are fenced with plain ``` which triggers MD040 (fenced-code-language). Adding a language (e.g. `sh`) will fix the lint and improve rendering:

-```
+```sh
 e.g.

 ADMIN_GOOGLE_SA_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n******\n-----END PRIVATE KEY-----\n"
 ADMIN_GOOGLE_SA_EMAIL=XXXX@XXXX-XXXX.iam.gserviceaccount.com
-```
+```
@@
-```
+```sh
 AUTH_USERNAME=myuser 
 AUTH_PASSWORD=secret
-```
+```

188-189: Nit: Capitalize “Google Sheets” in env var descriptions

In the env var table, the descriptions for ADMIN_GOOGLE_SA_PRIVATE_KEY and ADMIN_GOOGLE_SA_EMAIL say “your google sheets”. For polish and consistency with earlier sections, consider changing to “your Google Sheets”.

-| 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         |
+| 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         |
services/GoogleSheetsService.ts (1)

15-37: Consider validating required env vars before constructing the Sheets client

getUserSheetsClient assumes ADMIN_GOOGLE_SA_PRIVATE_KEY, ADMIN_GOOGLE_SA_EMAIL, and SPREADSHEET_ID are all set. If any are missing, the JWT or Sheets calls will fail with less-obvious runtime errors.

You could fail fast with clearer diagnostics:

 export async function getUserSheetsClient () {
   const session = await getSession() // next-auth v5 app router helper
 
   if (!session) {
     throw new Error('Not authenticated with Google')
   }
 
+  if (!ADMIN_GOOGLE_SA_EMAIL || !ADMIN_GOOGLE_SA_PRIVATE_KEY || !SPREADSHEET_ID) {
+    throw new Error('Google Sheets service account env vars are not fully configured')
+  }
+
   try {
@@
   } catch (err: any) {
     throw new Error(`Google Service Account error: ${err.message}`)
   }
 }

This makes deployment misconfigurations much easier to spot.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f6a441 and 7770295.

📒 Files selected for processing (8)
  • README.md (8 hunks)
  • app/admin/SessionProviderWrapper.tsx (1 hunks)
  • app/auth.ts (1 hunks)
  • components/Admin/Announcement/AddAnnouncement.tsx (1 hunks)
  • components/Admin/Announcement/AnnoucementForm.tsx (1 hunks)
  • components/Announcement/Announcement.tsx (1 hunks)
  • components/Announcement/AnnouncementModal.tsx (1 hunks)
  • services/GoogleSheetsService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/auth.ts
  • components/Announcement/Announcement.tsx
  • app/admin/SessionProviderWrapper.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
components/Admin/Announcement/AddAnnouncement.tsx (5)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
components/ui/LoadingSpinner.tsx (1)
  • LoadingSpinner (3-11)
components/ui/Buttons.tsx (1)
  • PrimaryButton (4-24)
components/ui/Modal.tsx (1)
  • Modal (21-91)
components/Admin/Announcement/AnnoucementForm.tsx (1)
  • AnnouncementForm (13-172)
components/Admin/Announcement/AnnoucementForm.tsx (3)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
services/MosqueDataService.ts (1)
  • createAnnouncement (110-112)
components/ui/LoadingSpinner.tsx (1)
  • LoadingSpinner (3-11)
services/GoogleSheetsService.ts (2)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
types/DailyPrayerTimeType.d.ts (1)
  • DailyPrayerTime (1-11)
🪛 LanguageTool
README.md

[style] ~99-~99: Consider using a different verb to strengthen your wording.
Context: ...n generate your own endpoint but please make sure it has all the required properties. ##...

(MAKE_SURE_ENSURE)


[uncategorized] ~188-~188: “Google” is a proper noun and needs to be capitalized.
Context: ...f Admin interface to interact with your google sheets | | ADMIN_GOOGLE_SA_EMAI...

(A_GOOGLE)


[uncategorized] ~189-~189: “Google” is a proper noun and needs to be capitalized.
Context: ...f Admin interface to interact with your google sheets | | AUTH_USERNAME ...

(A_GOOGLE)

🪛 markdownlint-cli2 (0.18.1)
README.md

22-22: Link text should be descriptive

(MD059, descriptive-link-text)


129-129: Bare URL used

(MD034, no-bare-urls)


134-134: Bare URL used

(MD034, no-bare-urls)


146-146: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


155-155: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


179-179: Bare URL used

(MD034, no-bare-urls)


189-189: Bare URL used

(MD034, no-bare-urls)


213-213: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (1)
services/GoogleSheetsService.ts (1)

149-212: Prayer‑times mapping fix looks consistent with DailyPrayerTime now

Initializing obj as {} and explicitly treating month, month_label, day_of_month, and sunrise_start as top‑level fields resolves the earlier runtime error and mis‑mapping. The remaining headers are correctly funneled into nested prayer objects (with the ASR first/second special cases), which aligns with the DailyPrayerTime / PrayerTime shapes.

Comment on lines 25 to 36
// Set default date = today
// Default start_time = 2 min from now
useEffect(() => {
const now = new Date()

// Date YYYY-MM-DD
setDate(now.toISOString().split('T')[0])

// Start time = now + 2 minutes
const start = new Date(now.getTime())
setStartTime(start.toISOString().slice(11, 16)) // HH:MM
}, [])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: “2 min from now” default and timezone handling don’t match the implementation

The effect comment says start time should be “now + 2 minutes”, but:

  • const start = new Date(now.getTime()) doesn’t add 2 minutes.
  • Using toISOString().slice(11, 16) gives UTC time, so on non‑UTC clients the default time will not match the user’s local clock.

This will produce a surprising default start time for admins.

You can lean on moment (already imported) to both add the 2 minutes and respect local time:

-  useEffect(() => {
-    const now = new Date()
-
-    // Date YYYY-MM-DD
-    setDate(now.toISOString().split('T')[0])
-
-    // Start time = now + 2 minutes
-    const start = new Date(now.getTime())
-    setStartTime(start.toISOString().slice(11, 16)) // HH:MM
-  }, [])
+  useEffect(() => {
+    const start = moment().add(2, 'minutes')
+
+    setDate(start.format('YYYY-MM-DD'))
+    setStartTime(start.format('HH:mm'))
+  }, [])
📝 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
// Set default date = today
// Default start_time = 2 min from now
useEffect(() => {
const now = new Date()
// Date YYYY-MM-DD
setDate(now.toISOString().split('T')[0])
// Start time = now + 2 minutes
const start = new Date(now.getTime())
setStartTime(start.toISOString().slice(11, 16)) // HH:MM
}, [])
// Set default date = today
// Default start_time = 2 min from now
useEffect(() => {
const start = moment().add(2, 'minutes')
setDate(start.format('YYYY-MM-DD'))
setStartTime(start.format('HH:mm'))
}, [])
🤖 Prompt for AI Agents
In components/Admin/Announcement/AnnoucementForm.tsx around lines 25 to 36, the
effect that sets the default date/time currently doesn’t add 2 minutes to the
start_time and uses toISOString() which returns UTC, causing incorrect local
times; change the logic to use the existing moment import to get the local date
and time, set date from moment().format('YYYY-MM-DD'), compute start time with
moment().add(2, 'minutes') and format it as moment().format('HH:mm') so the
default is actually now+2 minutes in the user’s local timezone.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
services/GoogleSheetsService.ts (1)

53-83: Fix return type and null-handling inconsistency.

Two issues:

  1. Type mismatch: Line 53 declares Promise<string> but line 78 can return null.
  2. Behavior inconsistency: sheetsGetAnnouncement (line 42) checks for null, but this function throws on missing keys (line 66) instead of returning null, so that check never executes as intended.

Based on the caller's expectation and the past review suggestion, update the return type and eliminate the throw for missing keys:

-async function sheetsGetMetadataValueByKey (key: string): Promise<string> {
+async function sheetsGetMetadataValueByKey (key: string): Promise<string | null> {
   try {
     const sheets = await getUserSheetsClient()
     const metadataValueRange = await sheets.spreadsheets.values.get({
       spreadsheetId: SPREADSHEET_ID,
       range: SHEET_NAME_METADATA,
     })
     const rows = metadataValueRange.data.values || []
     const targetIndex = rows.findIndex(
       (row) => row[0]?.trim() === key,
     )
 
     if (targetIndex === -1) {
-      throw new Error(
-        `Could not find '${key}' row in sheet ${SHEET_NAME_METADATA}`)
+      // Key not present in Metadata sheet
+      return null
     }
 
     // The value is in column B = column index 1
     const cell = `${SHEET_NAME_METADATA}!B${targetIndex + 1}`
 
     const data = await sheets.spreadsheets.values.get({
       spreadsheetId: SPREADSHEET_ID,
       range: cell,
     })
 
     return data?.data?.values?.[0]?.[0] ?? null
   } catch (error: any) {
     console.error(error)
     throw new Error(`Google Sheets API request failed: ${error?.message}`)
   }
 }
🧹 Nitpick comments (3)
services/GoogleSheetsService.ts (3)

8-10: Validate required environment variables at module load.

The service account credentials are required for all operations but aren't validated upfront. If ADMIN_GOOGLE_SA_PRIVATE_KEY or ADMIN_GOOGLE_SA_EMAIL are undefined, you'll get obscure JWT errors later. Consider throwing early or logging a clear warning if these are missing.

 const SPREADSHEET_ID = process.env.SPREADSHEET_ID ?? ''
 const ADMIN_GOOGLE_SA_PRIVATE_KEY = process.env.ADMIN_GOOGLE_SA_PRIVATE_KEY
 const ADMIN_GOOGLE_SA_EMAIL = process.env.ADMIN_GOOGLE_SA_EMAIL
+
+if (!ADMIN_GOOGLE_SA_PRIVATE_KEY || !ADMIN_GOOGLE_SA_EMAIL) {
+  throw new Error('Missing required environment variables: ADMIN_GOOGLE_SA_PRIVATE_KEY and ADMIN_GOOGLE_SA_EMAIL must be set')
+}

85-121: Consider extracting duplicate target-finding logic.

Lines 89-101 duplicate the target-row lookup from sheetsGetMetadataValueByKey (lines 56-68). A small helper would reduce repetition:

async function findMetadataRowIndex(sheets: any, key: string): Promise<number> {
  const metadataValueRange = await sheets.spreadsheets.values.get({
    spreadsheetId: SPREADSHEET_ID,
    range: SHEET_NAME_METADATA,
  })
  const rows = metadataValueRange.data.values || []
  const targetIndex = rows.findIndex((row) => row[0]?.trim() === key)
  
  if (targetIndex === -1) {
    throw new Error(`Could not find '${key}' row in sheet ${SHEET_NAME_METADATA}`)
  }
  
  return targetIndex
}

Then use it in both functions. Since the logic is short, deferring this is fine.


156-157: The @ts-ignore comment is unnecessary.

Line 157 already types obj as any, so TypeScript won't complain. The @ts-ignore can be removed unless it was addressing a different error.

-    //@ts-ignore
     const obj: any = { }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7770295 and d97c308.

📒 Files selected for processing (4)
  • components/Admin/Announcement/AnnoucementForm.tsx (1 hunks)
  • components/ui/Buttons.tsx (1 hunks)
  • components/ui/Modal.tsx (1 hunks)
  • services/GoogleSheetsService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/Admin/Announcement/AnnoucementForm.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
services/GoogleSheetsService.ts (3)
app/auth.ts (1)
  • getSession (82-82)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
types/DailyPrayerTimeType.d.ts (1)
  • DailyPrayerTime (1-11)
🔇 Additional comments (7)
components/ui/Buttons.tsx (1)

11-25: LGTM! Children are now rendered correctly.

The button implementation is solid. The component properly renders children, forwards all props including onClick, and includes comprehensive styling for various states (hover, active, focus, disabled). The past issue about missing children has been resolved.

components/ui/Modal.tsx (3)

29-33: LGTM! SSR hydration issue properly addressed.

The mounted state pattern correctly prevents SSR hydration mismatches. By returning null until the component is mounted client-side, both server and initial client render produce the same output, then the portal is created only after hydration is complete. This resolves the issue flagged in past reviews.

Also applies to: 47-48


56-56: Verify that disabling backdrop-click-to-close is intentional.

The backdrop onClick handler is commented out (line 56), which means users cannot close the modal by clicking outside of it. They must use the ESC key or the close button. While this prevents accidental dismissal, it differs from common modal UX patterns and may not meet user expectations.

If this restriction is intentional for specific dialogs (e.g., critical confirmations), consider:

  1. Making it configurable via a prop like closeOnBackdropClick?: boolean
  2. Documenting this behavior for consumers of the Modal component

If accidental, uncomment line 56:

-      // onClick={onClose} // click on backdrop closes
+      onClick={onClose} // click on backdrop closes

36-45: LGTM! ESC key handler implemented correctly.

The keyboard event listener properly closes the modal on Escape key press, with correct cleanup and dependencies. The effect only attaches the listener when the modal is open, preventing unnecessary event handling.

services/GoogleSheetsService.ts (3)

15-37: LGTM!

The session validation and JWT authentication setup follow best practices for Google Sheets API integration.


48-51: LGTM!

Clean wrapper for announcement updates.


159-211: LGTM!

The prayer time mapping logic correctly handles top-level keys, ASR special cases, and generic prayer attributes. The fixes from the previous review have been properly applied.

Comment on lines +4 to +9
export function PrimaryButton ({
//@ts-ignore
onClick,
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove unnecessary //@ts-ignore directive.

The //@ts-ignore on line 5 is unnecessary because React.ButtonHTMLAttributes<HTMLButtonElement> already includes onClick with the correct type (React.MouseEventHandler<HTMLButtonElement> | undefined). Removing this directive will restore TypeScript's type checking without breaking functionality.

Apply this diff:

 export function PrimaryButton ({
-  //@ts-ignore
   onClick,
   children,
   ...props
 }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
📝 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
export function PrimaryButton ({
//@ts-ignore
onClick,
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
export function PrimaryButton ({
onClick,
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
🤖 Prompt for AI Agents
components/ui/Buttons.tsx around lines 4 to 9: remove the unnecessary
"//@ts-ignore" above the onClick prop so TypeScript can type-check props
normally; simply delete that directive and ensure the component signature
remains "export function PrimaryButton({ onClick, children, ...props }:
React.ButtonHTMLAttributes<HTMLButtonElement>)" (no other changes required).

</div>
<ServiceWorker />
</main>
<Announcement />
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to make this conditional? Will it break if things aren't set up?

Comment on lines 27 to 35
useEffect(() => {
const now = new Date()

// Date YYYY-MM-DD
setDate(now.toISOString().split('T')[0])

const start = new Date(now.getTime())
setStartTime(start.toISOString().slice(11, 16)) // HH:MM
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

Feels quite manual - can we use momentjs to get the information we need?

Comment on lines +30 to +33
// Only handle GET requests
if (e.request.method !== "GET") {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

what was the reason we needed to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

without this POST fetch calls were being cached, I assumed the only http call we want to cache is GET

* Converts the PrayerTimes spreadsheet values into the DailyPrayerTime json schema
* @param values
*/
function prayerTimeValuesToPrayerTimesJsonSchema (values: any[][] | null = []): DailyPrayerTime[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why we needed this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept this as I ran a POC to fetch prayer times via the sheets, this way we wont need to pass the data to the mosque tech api and the screen can handle it all - can remove for now

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
components/Admin/Announcement/AnnoucementForm.tsx (1)

25-33: Code comment claims "2 min from now" but implementation doesn't add 2 minutes.

Line 26 states the default start time should be "2 min from now", but line 32 sets it to the current time without adding 2 minutes. This discrepancy was flagged in a previous review but remains unresolved.

Update the implementation to match the comment's intent:

  useEffect(() => {
-   const now = moment()
+   const now = moment().add(2, 'minutes')

    // Date YYYY-MM-DD
    setDate(now.format('YYYY-MM-DD'))
    setStartTime(now.format('HH:mm'))
  }, [])
🧹 Nitpick comments (3)
components/Admin/Announcement/AnnoucementForm.tsx (3)

51-60: Clear previous error state at submission start.

When resubmitting after a failed attempt, the previous error message will still be displayed until the new submission completes. Clear the error state when starting a new submission.

+   setError(null)
    setIsLoading(true)
    try {

76-76: Replace as any with proper type assertion.

Using as any bypasses TypeScript's type checking. Since the value comes from a <select> with only 'General' and 'Car' options, you can safely assert it as the union type.

-         onChange={(e) => setType(e.target.value as any)}
+         onChange={(e) => setType(e.target.value as 'General' | 'Car')}

112-119: Consider adding a reasonable maximum duration.

Without a max attribute, users can enter unreasonably large duration values (e.g., 9999999 minutes). Consider adding a sensible upper bound for better UX and data integrity.

        <input
          type="number"
          min={1}
+         max={1440}
          className="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm bg-slate-50 focus:ring-2 focus:ring-mosqueBrand-onPrimary outline-none"
          value={duration}
          onChange={(e) => setDuration(Number(e.target.value))}
          disabled={isLoading}
        />

Note: 1440 minutes = 24 hours. Adjust based on your requirements.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d97c308 and 9e958be.

📒 Files selected for processing (3)
  • app/admin/page.tsx (1 hunks)
  • app/layout.tsx (1 hunks)
  • components/Admin/Announcement/AnnoucementForm.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/admin/page.tsx (6)
app/layout.tsx (1)
  • metadata (6-15)
app/admin/SessionProviderWrapper.tsx (1)
  • SessionProviderWrapper (10-12)
app/auth.ts (3)
  • isAdminInterfaceEnabled (82-82)
  • getSession (82-82)
  • redirect (56-59)
types/MosqueDataType.d.ts (1)
  • MosqueMetadataType (10-10)
services/MosqueDataService.ts (1)
  • getMetaData (86-90)
components/Admin/AdminPage.tsx (1)
  • AdminPage (5-33)
components/Admin/Announcement/AnnoucementForm.tsx (3)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
services/MosqueDataService.ts (1)
  • createAnnouncement (110-112)
components/ui/LoadingSpinner.tsx (1)
  • LoadingSpinner (3-11)
🔇 Additional comments (3)
app/layout.tsx (1)

8-8: LGTM! Correct Next.js metadata template syntax.

The change from "%" to "%s" aligns with Next.js metadata template conventions for string substitution.

Also applies to: 12-12

app/admin/page.tsx (2)

10-13: LGTM! Metadata export is correctly structured.

The metadata object will be properly interpolated by the parent layout's "%s" template.


15-24: LGTM! Clean component structure with proper session context.

The SessionProviderWrapper correctly provides NextAuth session context for the admin page.

redirect("/api/auth/signin")
}

const mosqueMetadata: MosqueMetadataType = await getMetaData()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for metadata fetch.

The getMetaData() call can fail (network errors, API issues, etc.), but there's no error handling. If it throws, the entire admin page will crash with an unhandled exception.

Wrap the fetch in a try-catch block:

+  let mosqueMetadata: MosqueMetadataType
+  try {
+    mosqueMetadata = await getMetaData()
+  } catch (error) {
+    return (
+      <div className="min-h-screen flex items-center justify-center bg-red-50 px-6">
+        <div className="text-center">
+          <h1 className="text-2xl font-semibold mb-2 text-red-700">Error Loading Data</h1>
+          <p className="text-sm text-red-600">Unable to fetch mosque metadata. Please try again later.</p>
+        </div>
+      </div>
+    )
+  }
-  const mosqueMetadata: MosqueMetadataType = await getMetaData()

  return (
📝 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
const mosqueMetadata: MosqueMetadataType = await getMetaData()
let mosqueMetadata: MosqueMetadataType
try {
mosqueMetadata = await getMetaData()
} catch (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-red-50 px-6">
<div className="text-center">
<h1 className="text-2xl font-semibold mb-2 text-red-700">Error Loading Data</h1>
<p className="text-sm text-red-600">Unable to fetch mosque metadata. Please try again later.</p>
</div>
</div>
)
}
🤖 Prompt for AI Agents
In app/admin/page.tsx around line 50, the direct call const mosqueMetadata:
MosqueMetadataType = await getMetaData() lacks error handling and will crash the
page if getMetaData() throws; wrap the await in a try-catch, catch and log the
error (or surface it to your logger), and provide a safe fallback path—either
set mosqueMetadata to a sensible default/null and render an error state, or
rethrow/return a 500/notFound response after logging, so the page does not crash
on fetch failures.

Comment on lines +127 to +134
<input
type="text"
placeholder=""
className="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm bg-slate-50 focus:ring-2 focus:ring-mosqueBrand-onPrimary outline-none"
value={carReg}
onChange={(e) => setCarReg(e.target.value)}
disabled={isLoading}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation for required fields.

The form allows submission with empty message and empty carReg (when type is 'Car'). This creates announcements with missing critical information.

Add required attributes and validation:

          <input
            type="text"
            placeholder=""
            className="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm bg-slate-50 focus:ring-2 focus:ring-mosqueBrand-onPrimary outline-none"
            value={carReg}
            onChange={(e) => setCarReg(e.target.value)}
            disabled={isLoading}
+           required
          />
        <textarea
          rows={3}
          className="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm bg-slate-50 focus:ring-2 focus:ring-mosqueBrand-onPrimary outline-none"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          disabled={isLoading}
+         required
        />

Also applies to: 141-147

🤖 Prompt for AI Agents
In components/Admin/Announcement/AnnoucementForm.tsx around lines 127-134 (and
similarly 141-147), the input fields allow submission when required values are
empty; add client-side validation by marking the relevant inputs as required and
preventing form submit when invalid: add required attribute to the message
textarea and to the carReg input (or conditionally required when type ===
'Car'), validate in the form submit handler to check message.trim() is non-empty
and if type === 'Car' ensure carReg.trim() is non-empty, set and display a local
error state for each field and do not call the submit API when validation fails
(focus the first invalid field and optionally set an isLoading flag only after
validation passes).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
components/Admin/Announcement/AnnoucementForm.tsx (2)

128-135: Add required attribute to car registration input.

When the announcement type is 'Car', the car registration number is critical information but the input field doesn't have a required attribute. This allows empty car registration submissions, creating incomplete announcements.

Apply this diff:

           <input
             type="text"
             placeholder=""
             className="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm bg-slate-50 focus:ring-2 focus:ring-mosqueBrand-onPrimary outline-none"
             value={carReg}
             onChange={(e) => setCarReg(e.target.value)}
             disabled={isLoading}
+            required
           />

25-33: Implement the "+2 minutes" default as stated in the comment.

The comment says "Default start_time = 2 min from now" but the code sets startTime to the current time without adding 2 minutes. This creates a mismatch between the stated intent and actual behavior.

Apply this diff to add the 2-minute offset:

   useEffect(() => {
-    const now = moment()
+    const start = moment().add(2, 'minutes')
 
     // Date YYYY-MM-DD
-    setDate(now.format('YYYY-MM-DD'))
-    setStartTime(now.format('HH:mm'))
+    setDate(start.format('YYYY-MM-DD'))
+    setStartTime(start.format('HH:mm'))
   }, [])
🧹 Nitpick comments (2)
components/Admin/Announcement/AnnoucementForm.tsx (2)

76-76: Replace as any with proper type assertion.

Using as any bypasses TypeScript's type checking. Since the type is a strict union of 'General' | 'Car', you can safely assert to that type instead.

Apply this diff:

-          onChange={(e) => setType(e.target.value as any)}
+          onChange={(e) => setType(e.target.value as 'General' | 'Car')}

56-57: Improve error message formatting.

Direct string interpolation of the error object may produce unhelpful messages like "Error creating announcement: [object Object]". Extract the error message properly to provide better feedback to the user.

Apply this diff:

-    } catch(error) {
-      setError(`Error creating announcement: ${error}`)
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error)
+      setError(`Error creating announcement: ${errorMessage}`)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e958be and bfc8f7f.

📒 Files selected for processing (1)
  • components/Admin/Announcement/AnnoucementForm.tsx (1 hunks)
🔇 Additional comments (1)
components/Admin/Announcement/AnnoucementForm.tsx (1)

35-62: Excellent fix for async handling!

The handleSubmit function now properly awaits createAnnouncement and only calls onComplete after successful submission. This ensures the modal doesn't close prematurely and prevents state updates on unmounted components.

… where the data will be stored as key value as flattened json

This change will allow data to be easily readable and editable in sheets as well as giving the flexibility to store complex data.

- Added jest for testing the logic of converting json to rows and rows to json
@mahfuzhannan
Copy link
Contributor Author

New configuration sheet:
image

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
jest.config.js (1)

1-17: Config is solid; consider preset spreading and DOM-test environment strategy

This setup is correct for ts-jest and the jose ESM edge case; nothing blocking here. Two optional refinements to consider:

  1. Spread the whole ts-jest preset, not just transform
    This makes the config more future-proof if createDefaultPreset() ever adds more defaults than transform:
-const tsJestTransformCfg = createDefaultPreset().transform;
+const tsJestPreset = createDefaultPreset();

 /** @type {import("jest").Config} **/
 module.exports = {
-  testEnvironment: "node",
-  transform: {
-    ...tsJestTransformCfg,
-  },
+  ...tsJestPreset,
+  testEnvironment: "node",
   moduleNameMapper: {
     '^@/(.*)$': '<rootDir>/$1',
   },
   transformIgnorePatterns: [
     'node_modules/(?!(jose)/)',
   ],
 };
  1. Double‑check testEnvironment: "node" vs future React/DOM tests
    For server-side / NextAuth / jose-heavy tests, "node" is a good choice and often avoids jose + jsdom issues. If you later add React Testing Library / DOM-based component tests, you may want either:
    • A separate Jest config using testEnvironment: "jsdom" for UI tests, or
    • Per-test overrides via /** @jest-environment jsdom */ at the top of those files.
services/GoogleSheetsUtil.test.ts (1)

1-49: Consider adding test coverage for sheetsUtilValuesToJson.

The test suite imports and tests sheetsUtilFlattenedJsonToRows and sheetsUtilValuesToNestedJson, but sheetsUtilValuesToJson is also imported and exported from GoogleSheetsUtil.ts (line 8 in GoogleSheetsUtil.ts). Adding test coverage for this function would ensure all public utilities are validated.

types/MosqueDataType.d.ts (1)

17-17: Consider narrowing ConfigurationJson type if possible.

The type ConfigurationJson = { [key: string]: any } is very permissive. If the structure is known (e.g., it contains announcement?: AnnouncementData), consider defining a more specific interface to improve type safety and catch errors at compile time.

services/GoogleSheetsUtil.ts (1)

38-55: Consider validating keys before processing.

The function doesn't validate whether key is empty or contains only dots, which could lead to unexpected behavior. Consider adding validation:

 const key = String(rawKey);
+if (!key || key.trim() === '') continue;
+
 const parts = key.split("."); // Split on dots to handle nested keys
+if (parts.some(p => !p)) continue; // Skip if any part is empty
services/GoogleSheetsService.ts (2)

19-41: Consider clarifying the session requirement for service account operations.

The function checks for a user session (line 20) but then authenticates with a Google Service Account using JWT (lines 28-32). Service Accounts don't require user sessions—they use their own credentials.

If the intent is to restrict access to authenticated admin users only, this makes sense. However, consider renaming the function to better reflect its purpose (e.g., getAuthenticatedSheetsClient) and adding a comment explaining that the session check is for authorization, not for Google Sheets authentication.


67-82: Consider extracting visibility computation to avoid duplication.

The visibility computation logic (lines 74-78) is duplicated in services/MosqueDataService.ts (lines 102-106). Consider extracting this into a shared utility function to maintain consistency and reduce duplication.

Example:

// Create a new utility function
function computeAnnouncementVisibility(announcement: AnnouncementData): boolean {
  if (!announcement.date || !announcement.start_time || !announcement.end_time) {
    return false
  }
  
  const now = moment()
  return (
    now.isSame(announcement.date, 'day')
    && now.isSameOrAfter(`${announcement.date} ${announcement.start_time}`, 'minutes')
    && now.isBefore(`${announcement.date} ${announcement.end_time}`, 'minutes')
  )
}
package.json (1)

20-20: Consider updating googleapis to version 166.0.0.

The installed version 105.0.0 is significantly outdated (latest stable is 166.0.0 as of December 2025). While no known security vulnerabilities were found in the 105.x branch, updating to the latest version is recommended to benefit from bug fixes, performance improvements, and security patches accumulated across 61 major releases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfc8f7f and a43799d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • app/api/data/announcements/route.ts (1 hunks)
  • jest.config.js (1 hunks)
  • package.json (2 hunks)
  • services/GoogleSheetsService.ts (1 hunks)
  • services/GoogleSheetsUtil.test.ts (1 hunks)
  • services/GoogleSheetsUtil.ts (1 hunks)
  • services/MosqueDataService.ts (2 hunks)
  • tsconfig.json (2 hunks)
  • types/MosqueDataType.d.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tsconfig.json
  • app/api/data/announcements/route.ts
🧰 Additional context used
🧬 Code graph analysis (2)
services/MosqueDataService.ts (2)
types/AnnouncementType.d.ts (1)
  • AnnouncementData (1-10)
services/GoogleSheetsService.ts (1)
  • sheetsUpdateAnnouncement (84-88)
services/GoogleSheetsUtil.test.ts (1)
services/GoogleSheetsUtil.ts (2)
  • sheetsUtilFlattenedJsonToRows (84-101)
  • sheetsUtilValuesToNestedJson (25-59)
🔇 Additional comments (8)
services/GoogleSheetsUtil.ts (2)

8-23: LGTM!

The function correctly handles empty arrays, uses the first row as headers, and safely handles missing values with the nullish coalescing operator.


84-101: Consider documenting array handling behavior in JSDoc.

The implementation correctly flattens nested objects by recursively walking the structure. Arrays will be treated as objects with numeric keys (e.g., arr[0], arr[1]). If this is the intended behavior, document it in the JSDoc. If arrays should be handled differently, update the implementation accordingly.

services/MosqueDataService.ts (2)

92-109: Verify handling of missing time fields.

The visibility computation (lines 102-106) assumes start_time and end_time are present. If these can be null or undefined (as suggested by the AnnouncementData type which marks some fields as optional), the string concatenation on lines 104-105 could produce invalid date-time strings like "2025-12-05 undefined", leading to incorrect visibility calculations.

Check the AnnouncementData type definition and consider adding guards:

+ if (!announcement.start_time || !announcement.end_time) {
+   announcement.is_visible = false
+   return announcement
+ }
+
  announcement.is_visible = (
    now.isSame(announcement?.date, 'day')
    && now.isSameOrAfter(`${announcement?.date} ${announcement?.start_time}`, 'minutes')
    && now.isBefore(`${announcement?.date} ${announcement?.end_time}`, 'minutes')
  )

111-113: LGTM!

Clean delegation to the Google Sheets service. Input validation is appropriately handled at other layers.

services/GoogleSheetsService.ts (4)

43-51: LGTM!

Clean readiness check with appropriate error handling and logging.


53-65: LGTM!

The function correctly fetches and transforms the configuration data with appropriate error handling.


84-88: LGTM!

Clean implementation that correctly updates the announcement within the configuration data.


90-102: LGTM!

The function correctly flattens the configuration data and updates the sheet with appropriate options.

"moment": "^2.29.4",
"moment-hijri": "^2.1.2",
"next": "^15.4.1",
"next-auth": "^4.24.13",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

next-auth 4.24.13 Next.js 15.4.1 compatibility

💡 Result:

Short answer: Possibly, but not fully guaranteed — next-auth 4.24.13 contains several backports to improve compatibility with Next.js 14/15, but active issues remain for Next.js 15 (App Router / React 19) and some users report runtime/type errors. Test carefully or use v5 (Auth.js) which targets Next 15.

Sources:

  • next-auth GitHub issues reporting Next.js 15/App Router problems and fixes/backports. [1]
  • Security/npm page showing latest 4.24.13 release. [2]

Recommendations:

  • If you must run Next.js 15 in production: test end-to-end (app router route handlers, server components, React 19) — if you hit issues, consider upgrading to Auth.js (next-auth v5 / auth.js) which has explicit Next 15 support, or pin Next/React versions (e.g., React 18) until fixes land.

🌐 Web query:

next-auth v5 release status stable beta

💡 Result:

Short answer: As of Dec 5, 2025, NextAuth (Auth.js) v5 is still published as beta — maintainers continue releasing 5.0.0-beta.x versions while v4 remains the designated stable line. [1][2]

Sources:

  • Release listing showing 5.0.0-beta releases (latest beta entries). [1]
  • GitHub discussion/issues noting v5’s beta status and docs request. [2]

🌐 Web query:

next-auth version requirements Next.js support matrix

💡 Result:

Short compatibility matrix (community/maintainer sources):

  • next-auth v4 (stable): officially tested/peer range historically: Next.js ^12.2.5 || ^13 (and later patched to include 14/15 in point releases). See v4 docs/releases and related GitHub issues. [1][2]

  • next-auth v5 / "next-auth" (beta; rebranded toward Auth.js): minimum Next.js noted in upgrade guides/beta docs is Next.js 14.0+. v5 was released as beta/early-stable for Next.js 14+; ecosystem issues show peer-range gaps for Next.js 16 as of late 2025. If you need production stability, v4 is recommended until v5 reaches GA for your Next version. [3][4]

Notes / practical advice:

  • Check the package peerDependencies before installing (npm/yarn will show conflicts). [1][4]
  • For cutting-edge Next.js (15/16+), you may need beta/updated next-auth (or --legacy-peer-deps) until maintainers add explicit support; follow the next-auth GitHub for PRs/releases. [1][3]

Sources:

  1. NextAuth.js docs / upgrade notes (v4). [turn0search4] (NextAuth.js upgrade guide).
  2. npm package / release info for next-auth (shows v4 releases). [turn1search0] (npm next-auth).
  3. NextAuth v5 upgrade / community guides noting Next.js 14+ minimum for v5. [turn0search7] (v5 upgrade guide).
  4. GitHub issues about peerDependency/compatibility (Next.js 14–16 compatibility discussions). [turn1search3] (next-auth GitHub issue).

Address next-auth version compatibility with Next.js 15.4.1.

next-auth v4.24.13 has known compatibility issues with Next.js 15.4.1, including App Router and React 19 problems with active unresolved issues on GitHub. While point releases added backports, stability is not guaranteed. next-auth v5 (Auth.js) explicitly targets Next.js 14+ and provides better Next.js 15 support, but remains in beta. Consider either testing the current setup thoroughly across App Router route handlers and server components, or evaluating v5 despite its beta status if Next.js 15 stability is required.

Comment on lines +67 to +82
export async function sheetsGetAnnouncement(): Promise<AnnouncementData | null> {
const data = await sheetsGetConfigurationData()
let announcement = data?.announcement as unknown as AnnouncementData ?? null


const now = moment()

announcement.is_visible = (
now.isSame(announcement?.date, 'day')
&& now.isSameOrAfter(`${announcement?.date} ${announcement?.start_time}`, 'minutes')
&& now.isBefore(`${announcement?.date} ${announcement?.end_time}`, 'minutes')
)


return announcement
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix null handling before accessing announcement properties.

Line 69 assigns announcement which can be null, but line 74-78 accesses announcement.date, announcement.start_time, and announcement.end_time without null checks. This will cause a runtime error if the announcement is null.

 export async function sheetsGetAnnouncement(): Promise<AnnouncementData | null> {
   const data = await sheetsGetConfigurationData()
   let announcement = data?.announcement as unknown as AnnouncementData ?? null
 
+  if (!announcement || !announcement.date) {
+    return null
+  }
+
+  if (!announcement.start_time || !announcement.end_time) {
+    announcement.is_visible = false
+    return announcement
+  }
 
   const now = moment()
 
   announcement.is_visible = (
     now.isSame(announcement?.date, 'day')
     && now.isSameOrAfter(`${announcement?.date} ${announcement?.start_time}`, 'minutes')
     && now.isBefore(`${announcement?.date} ${announcement?.end_time}`, 'minutes')
   )
 
 
   return announcement
 }
📝 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
export async function sheetsGetAnnouncement(): Promise<AnnouncementData | null> {
const data = await sheetsGetConfigurationData()
let announcement = data?.announcement as unknown as AnnouncementData ?? null
const now = moment()
announcement.is_visible = (
now.isSame(announcement?.date, 'day')
&& now.isSameOrAfter(`${announcement?.date} ${announcement?.start_time}`, 'minutes')
&& now.isBefore(`${announcement?.date} ${announcement?.end_time}`, 'minutes')
)
return announcement
}
export async function sheetsGetAnnouncement(): Promise<AnnouncementData | null> {
const data = await sheetsGetConfigurationData()
let announcement = data?.announcement as unknown as AnnouncementData ?? null
if (!announcement || !announcement.date) {
return null
}
if (!announcement.start_time || !announcement.end_time) {
announcement.is_visible = false
return announcement
}
const now = moment()
announcement.is_visible = (
now.isSame(announcement?.date, 'day')
&& now.isSameOrAfter(`${announcement?.date} ${announcement?.start_time}`, 'minutes')
&& now.isBefore(`${announcement?.date} ${announcement?.end_time}`, 'minutes')
)
return announcement
}
🤖 Prompt for AI Agents
In services/GoogleSheetsService.ts around lines 67 to 82, announcement can be
null but the code unconditionally accesses announcement.date,
announcement.start_time and announcement.end_time; add a null guard before
computing is_visible (for example return null or set announcement.is_visible =
false when announcement is null) or early-return when announcement is null so
you never access properties on a null value; ensure subsequent code handles the
null return consistently.

Comment on lines +35 to +36
// Skip header row if present
if (rawKey === "key") continue;
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

Header detection may skip legitimate data rows.

The check if (rawKey === "key") continue; skips rows where the first column is exactly "key". If a legitimate configuration entry has "key" as its key name, it will be silently ignored. Consider using a more robust header detection mechanism (e.g., checking if this is the first row or using a special marker).

🤖 Prompt for AI Agents
In services/GoogleSheetsUtil.ts around lines 35-36, the current header detection
unconditionally skips any row whose first cell equals the string "key", which
can drop legitimate entries; instead only treat the first sheet row as a header
(or detect header by matching a known header row shape), so change the logic to
skip the header based on row index (e.g., skip when rowIndex === 0) or compare
the entire row to the expected header array before continuing, ensuring
legitimate rows with "key" as a value are not ignored.

@vercel
Copy link

vercel bot commented Dec 6, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
darul-khidmah Ready Ready Preview Comment Dec 6, 2025 11:17pm
darul-ummah-hornchurch Ready Ready Preview Comment Dec 6, 2025 11:17pm
mosque-prayer-display-screen Ready Ready Preview Comment Dec 6, 2025 11:17pm

@mahfuzhannan mahfuzhannan reopened this Dec 6, 2025
@DilwoarH DilwoarH merged commit 682e60f into MosqueOS:main Dec 6, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants