JustToThePoint English Website Version
JustToThePoint en español

Building the Verse Tab UI in React Native

The truth is multifaceted and complex. Life is always messier than our dreams and beliefs, reality is more complicated that we think it is, it is constantly changing like the wind and the future is not promised, but completely unpredictable, Anawim, #justtothepoint.

In this post we replace the previous test harness with the final Verse tab – a two‑column layout that shows the daily plan on the left and the full verse on the right. Along the way we’ll explore several React Native patterns that make a user interface feel quite polished and performant.

Martha loves Calculus 

What you will learn

What you will build

The complete Verse tab, replacing the previous test screen:

You’ll be able to:

FlatList ItemSeparatorComponent

A common mistake is to add a bottom border to every list item style. While it works, it also draws a border after the last item, which often looks unintentional.

// ❌ Border in item style — appears after the last item too
<View style={{ borderBottomWidth: 1, borderColor: "#2d3550" }}>
  <Text>{item.ref}</Text>
</View>

FlatList provides the ItemSeparatorComponent prop. React Native renders this component only between items — never after the last one. It keeps your item styles clean and is more performant because the separator is a separate lightweight component.

// ✅ ItemSeparatorComponent — Clean, no extra border after the last item
// Semantic separation – the separator is truly a separator, not part of the item
// No “last line” problem – no need to add special logic like index !== data.length - 1.
  ItemSeparatorComponent={() =>   ...
/>

TouchableHighlight vs TouchableOpacity

Both components make a view touchable and provide visual feedback, but they do it differently.

TouchableOpacity TouchableHighlight
Feedback effectt Reduces opacity of the child Shows a coloured underlay beneath the child
Best use case Buttons, icons, anything with a solid background List items (mimics native iOS/Android list presses)
underlayColor ❌ Not available ✅ Sets the highlight colour
Child requirement Any number of children Yes — it must have exactly one child (wrapped in a View)
// List item — TouchableHighlight gives a native‑like press
<TouchableHighlight
  underlayColor="#2a3050"    // // colour visible (shown) during press
  onPress={onPress}
>
  <View>   {/* ← this is the required single child */}
    <Text>{item}</Text>
  </View>
</TouchableHighlight>

Because it highlights the background rather than fading the content, TouchableHighlight is the preferred choice for rows in a list. It feels exactly like tapping a cell in your device’s Settings app.

⚠️ Remember: TouchableHighlight must have exactly one child. If you need multiple elements, wrap them in a single View.

Conditional rendering with && and ? :

React lets you embed logic directly inside JSX.

JSX stands for JavaScript XML. It is a syntax extension for JavaScript primarily used with React to describe what the user interface (UI) should look like. It allows you to write HTML-like structures directly inside your JavaScript files, combining your markup and rendering logic into a single place called a component.

// && — render only when the condition is true
{isToday && <View style={s.todayDot} />}

// Ternary — render one of two things (choose between two components)
{loading ? <Spinner /> : <VerseText />}

// verse?.ref — Optional chaining. (i) If verse is an object → returns verse.ref
// If verse is null or undefined → returns undefined instead of throwing an error
// ?? — Nullish coalescing. This operator chooses the left side unless it is null or undefined
// If verse?.ref is "John 3:16" → you get "John 3:16"
// If verse?.ref is undefined → you get "Loading…"
// If verse?.ref is null → you get "Loading…"
{verse?.ref ?? "Loading…"}

Step 1 — Update useVerse.ts to expose plan data

The hook currently only returns verse, date, loading, etc. The left list needs the complete array of references (planRefs) and the index of the currently displayed verse (planIndex).

Open src/hooks/useVerse.ts and make these additions:

Extend the UseVerseResult interface:

export interface UseVerseResult {
  // … existing fields …
  /** All references from daily_plan.txt (for the left sidebar) */
  planRefs:   string[];
  /** 0-based index of the currently displayed verse in planRefs */
  planIndex:  number;
}

Add new state variables at the top of the hook function:

const [planRefs,  setPlanRefs]  = useState<string[]>([]);
const [planIndex, setPlanIndex] = useState<number>(0);

Populate the plan data inside loadOffline(). Inside loadOffline(), after setVerse(offlineVerse), add:

// Load the full plan list for the sidebar
const { readLines } = await import("../utils/assetReader");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const refs = await readLines(require("../../data/daily_plan.txt"));
// The mountedRef check ensures we don’t update state if the component has already been unmounted
if (mountedRef.current) {
  setPlanRefs(refs);
  // Calculate index: same formula as planIndex() in dateUtils
  const jan1  = new Date(date.getFullYear(), 0, 1);
  const doy   = Math.floor((date.getTime() - jan1.getTime()) / 86_400_000) + 1;
  const idx   = refs.length > 0 ? (doy - 1) % refs.length : 0;
  setPlanIndex(idx);
}

Return the new fields. Add planRefs and planIndex to the object returned at the end of the hook.

planRefs,
planIndex,

Now our hook supplies everything the new UI needs.

Step 2 — Replace app/(tabs)/verse.tsx

// app/(tabs)/verse.tsx
//
// VERSE TAB — full UI (Chunk 18)
//
// Two-column layout mirroring the Prayers tab:
//   Left  (fixed width): FlatList of all references from daily_plan.txt
//   Right (flex 1):      Reference, source badge, verse text, and navigation
//
// DATA: useVerse hook provides all state and actions.
//       The hook exposes planRefs + planIndex for the left list.
// ═══════════════════════════════════════════════════════════════════════
// DESIGN NOTES
//
// * The screen uses a dark theme with a colour palette (C) to avoid
//   magic strings and make future tweaks easier.
//
// * The left sidebar width is responsive: 35 % of screen width, but
//   never wider than 200 pixels. This is the same formula used in the
//   Prayers tab, giving a consistent feel across the app.
//
// * The right column is the main content area. It's a ScrollView so
//   users can pull‑to‑refresh even when the verse text is short.
//
// * On very narrow phones (e.g. 320 px wide) the 35 % sidebar would be
//   112 px, leaving enough room for the verse text.
//
// * We use TouchableHighlight for list items (native feel) and
//   TouchableOpacity for occasional small buttons.
// ═══════════════════════════════════════════════════════════════════════
import React, { useCallback, useRef, useEffect } from "react";
import {
  View,
  Text,
  FlatList,
  TouchableHighlight,
  TouchableOpacity,
  ScrollView,
  StyleSheet,
  RefreshControl,
  ListRenderItemInfo,
  useWindowDimensions,
} from "react-native";
import { useVerse } from "../../src/hooks/useVerse";
import { LoadingSpinner } from "../../src/components/LoadingSpinner";
import { isToday, formatDate } from "../../src/utils/dateUtils";

// ── Colour palette ─────────────────────────────────────────────────────
// Centralising all colours here makes it trivial to adjust the theme
// later or to introduce a light mode. Each constant describes its
// typical usage.
const C = {
  bg:      "#1a1f2e",   // screen background
  panel:   "#1e2338",   // card / list item background
  border:  "#2d3550",   // subtle borders
  accent:  "#5b8fd4",   // interactive elements, links
  text:    "#dde4f0",   // main text
  muted:   "#9ca3af",   // secondary text, placeholders
  faint:   "#6b7280",   // very subtle text (debug labels)
  gold:    "#c9a84c",   // highlight colour for today / selection
  dim:     "#4b5563",   // footer, chunk labels
};


// ══════════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ══════════════════════════════════════════════════════════════════════════════
/**
 * VerseScreen
 *
 * The top‑level component for the Verse tab. It:
 *   1. Reads the current device width to calculate the sidebar width.
 *   2. Calls `useVerse()` to obtain all verse data and actions.
 *   3. Shows a full‑screen loading spinner **only** before the very
 *      first verse is loaded. After that, loading is always `false`
 *      because the offline KJV lookup is synchronous.
 *   4. Renders a two‑column layout: PlanList (left) and VerseDetail (right).
 *
 * The actual list and detail sub‑components are defined below to keep
 * this file self‑contained (no extra navigation or prop‑drilling).
 */
export default function VerseScreen() {
  // ── Hook & data ──────────────────────────────────────────────────────
  // `verse` is an object containing:
  //   - verse.result (the actual verse text, ref, source)
  //   - loading, error, source
  //   - date, setDate, refresh
  //   - planRefs: string[]      (all 63 daily references)
  //   - planIndex: number       (index of today's verse in planRefs)
  // See `src/hooks/useVerse.ts` for the full interface.
  const verse = useVerse();

  // ── Responsive sidebar width ─────────────────────────────────────────
  const { width: screenWidth } = useWindowDimensions();
  // The sidebar should never dominate the screen, even on wide tablets.
  // 35 % of the screen width, capped at 200 px.
  const sidebarWidth = Math.min(200, Math.floor(screenWidth * 0.35));
  // Example: on iPhone 14 (390 px)  → 136 px sidebar, 254 px for text.
  //          on iPad portrait (768) → 200 px sidebar, 568 px for text.

  // ── First‑load spinner ───────────────────────────────────────────────
  if (verse.loading && !verse.verse) {
    // Only show a spinner when the app is truly cold‑starting.
    // Once the offline KJV data is read (which is synchronous after
    // the first load), `loading` will remain `false` for all subsequent
    // date changes because the verse lookup is instant.
    return <LoadingSpinner message="Loading verse…" />;
  }

  // ── Main layout ──────────────────────────────────────────────────────
  return (
    <View style={s.root}>
      {/* Left column: the plan reference list */}
      <PlanList verse={verse} sidebarWidth={sidebarWidth} />
       {/* Vertical divider between the two columns */}
      <View style={s.divider} />
      {/* Right column: the verse content and navigation */}
      <VerseDetail verse={verse} />
    </View>
  );
}

// ══════════════════════════════════════════════════════════════════════════════
// LEFT COLUMN — PLAN LIST
//
// The PlanList component renders the full 63‑day reading plan in a vertical
// FlatList. It receives the full `useVerse` hook return object and the
// calculated sidebar width.
//
// Key behaviours:
//   - Automatically scrolls to today's verse on first render and whenever
//     `planIndex` changes (e.g., after tapping Prev/Today/Next).
//   - Each row is a `PlanItem` – a `TouchableHighlight` that navigates to
//     the corresponding date.
//   - Today’s row gets a gold left‑border and a small dot.
//   - Uses `getItemLayout` to make `scrollToIndex` precise and instant.
// ══════════════════════════════════════════════════════════════════════════════

function PlanList({
  verse,
  sidebarWidth,
}: {
  /** The full return value of `useVerse()` – gives us all state and actions */
  verse:        ReturnType<typeof useVerse>;
  /** Calculated width of the left sidebar (capped at 200px, 35% of screen) */
  sidebarWidth: number;
}) {
  // FlatList ref — lets us scroll to the selected item programmatically
  const listRef = useRef<FlatList>(null);
  // useRef is used here because we need a stable reference that persists
  // across renders and is not recreated on every state change. The ref is
  // then attached to the FlatList, giving us access to its scroll methods.

  // Scroll to the current plan index whenever it changes.
  useEffect(() => {
    // Do nothing if the plan hasn't loaded yet.
    if (verse.planRefs.length === 0) return;

    // We delay by 150ms to give React Native time to render the list rows.
    // Without this, scrollToIndex might fail because the target item isn't
    // yet measured.
    const timer = setTimeout(() => {
      listRef.current?.scrollToIndex({
        index:    verse.planIndex,
        animated: true,
        viewPosition: 0.4,
        // viewPosition: 0 means top of viewport, 0.5 centre, 1 bottom.
        // We use 0.4 so the selected item sits slightly above the vertical
        // centre, making the next few items visible below it.
      });
    }, 150);
    // Cleanup: cancel the timeout if the effect is re‑run (e.g. if the user
    // quickly taps multiple dates) to avoid conflicting scrolls.
    return () => clearTimeout(timer);
    // Dependencies: we only re‑run this effect when the selected index changes
    // or the plan list length changes (e.g., after first load).
  }, [verse.planIndex, verse.planRefs.length]);

  // --------------------------------------------------------------------------
  // Memoised renderItem callback
  // --------------------------------------------------------------------------
  // useCallback keeps the function identity stable between renders unless
  // its dependencies change. This prevents unnecessary re‑renders of every
  // list item when, for example, the verse text changes but selection hasn't.
  const renderItem = useCallback(
    // This is an arrow function (=>). It takes one argument, which is an object (a ListRenderItemInfo<string>)
    // That object is destructured into { item, index }.
    // It returns JSX (the  component)
    // In plain English, When FlatList asks how to render each row,
    // take the item and index, and return a PlanItem component.
    ({ item, index }: ListRenderItemInfo<string>) => (
      // Check if this row is today's date (for the dot indicator).
      <PlanItem
        ref_={item}
        index={index}
        /* The item is selected if its index matches the plan’s current index. */
        isSelected={index === verse.planIndex}
        /* It checks two things: The item is the selected one; The selected item corresponds to today’s date. */
        isTodayItem={index === verse.planIndex && isToday(verse.date.toISOString().slice(0, 10))}
        onPress={() => {
          // how tapping a plan item jumps to the correct date.
          // 1. Get today
          const today       = new Date();
          // 2. Compute day-of-year
          const jan1        = new Date(today.getFullYear(), 0, 1);
          const doy         = Math.floor((today.getTime() - jan1.getTime()) / 86_400_000) + 1;
          // 3. Convert day-of-year to today’s plan index
          const todayIdx    = (doy - 1) % verse.planRefs.length;
          // 4. Compute how far (i.e., offset) the tapped item is from today
          const daysDiff    = index - todayIdx;
          // 5. Create the target date
          const targetDate  = new Date(today);
          targetDate.setDate(today.getDate() + daysDiff);
          // 6. Update the verse date
          verse.setDate(targetDate);
        }}
      />
    ),
    // Dependencies: if any of these change, the renderItem function is
    // rebuilt so that the list reflects the new selection / date.
    [verse.planIndex, verse.planRefs.length, verse.setDate]
  );

  // Called when scrollToIndex fails (e.g., item not yet rendered).
  // FlatList requires this to handle off-screen items gracefully.
  const onScrollToIndexFailed = useCallback(
    (info: { index: number }) => {
      // This can happen if the target index hasn't been rendered yet
      // (e.g., user taps "Today" while the list is still loading items far
      // from the current scroll position). We wait 500ms for more items to
      // be laid out, then retry without animation.
      setTimeout(() => {
        // 2. Retry the scroll
        listRef.current?.scrollToIndex({
          index: info.index,
          animated: false,
        });
      // 1. Wait half a second for more items to mount
      }, 500);
    },
    // no dependencies – this fallback never changes
    []
  );

  // --------------------------------------------------------------------------
  // Render
  // --------------------------------------------------------------------------
  return (
    <View style={[s.sidebar, { width: sidebarWidth }]}>

       {/* Sidebar header shows the label and the number of plan entries */}
      <View style={s.sidebarHeader}>
        <Text style={s.sidebarLabel}>Reading Plan</Text>
        <Text style={s.sidebarCount}>{verse.planRefs.length}</Text>
      </View>

       {/*
        If the plan hasn't loaded yet (planRefs is empty), show a gentle
        "Loading plan…" message instead of an empty list.
      */}
      {verse.planRefs.length === 0 ? (
        <View style={s.sidebarEmpty}>
          <Text style={s.sidebarEmptyText}>
            Loading plan…
          </Text>
        </View>
      ) : (
        <FlatList
          ref={listRef}
          data={verse.planRefs}
          /*
            keyExtractor: each item is a string reference like "John 3:16".
            We prepend the index to guarantee uniqueness because duplicates
            are theoretically possible (though unlikely in a 63‑day plan).
          */
          keyExtractor={(item, index) => `${index}-${item}`}
          renderItem={renderItem}
          // ItemSeparatorComponent: React renders this ONLY between rows,
          // not after the last one. It's a thin 1px line.
          ItemSeparatorComponent={() => (
            <View style={s.separator} />
          )}
          onScrollToIndexFailed={onScrollToIndexFailed}
          showsVerticalScrollIndicator={false}
          initialScrollIndex={
            /* initialScrollIndex: forces the list to jump to today's verse
            immediately on first render, before any state change.
            We clamp to a valid range to avoid a red‑screen error if the
            plan data hasn't yet arrived when this prop is evaluated. */
            Math.max(0, Math.min(verse.planIndex, verse.planRefs.length - 1))
          }
          /*
            getItemLayout: a performance optimisation. Because every row has
            a fixed height (ITEM_HEIGHT), we can tell the list the exact
            position of every item. This lets scrollToIndex work without
            needing to measure any rows – it's instantly accurate.

            Signature: (data, index) => { length, offset, index }
              - length: height of the row
              - offset: distance from the top of the list to this row
              - index:  same as the input index
          */
          getItemLayout={(_, index) => ({
            length: ITEM_HEIGHT,
            offset: ITEM_HEIGHT * index,
            index,
          })}
        />
      )}
    </View>
  );
}

/*
  ITEM_HEIGHT: fixed height for each list row.
  Required for getItemLayout (above) and used in the PlanItem styling to
  ensure consistent sizing.
*/
const ITEM_HEIGHT = 52;

// ══════════════════════════════════════════════════════════════════════════════
// PLAN ITEM — individual row in the left column
// ══════════════════════════════════════════════════════════════════════════════
function PlanItem({
  ref_,
  index,
  isSelected,
  isTodayItem,
  onPress,
}: {
  /** The full reference string, e.g. "John 3:16" */
  ref_:       string;
  /** Zero‑based index in the plan array (for debugging if needed) */
  index:      number;
  /** Whether this row is the currently selected day */
  isSelected:  boolean;
  /** Whether this row corresponds to the actual calendar day (today) */
  isTodayItem: boolean;
  /** Callback when the row is tapped */
  onPress:     () => void;
}) {
  // Split the reference into book and chapter:verse for display.
  // We split on the last space so that books with spaces ("1 Corinthians")
  // are treated correctly.
  // "John 3:16"          → book="John", cv="3:16"
  // "1 Corinthians 13:4" → book="1 Corinthians", cv="13:4"
  const lastSpace = ref_.lastIndexOf(" ");
  const book = lastSpace >= 0 ? ref_.slice(0, lastSpace) : ref_;
  const cv   = lastSpace >= 0 ? ref_.slice(lastSpace + 1) : "";

  return (
     /*
      TouchableHighlight: used here because it gives a native‑like
      highlight effect on press (shows underlayColor behind the child).
      It must have exactly one child, so we wrap the content in a View.

      style={{ height: ITEM_HEIGHT }} ensures the row is exactly the
      fixed height expected by getItemLayout.
    */
    <TouchableHighlight
      underlayColor="#252d45"
      onPress={onPress}
      style={{ height: ITEM_HEIGHT }}
    >
      <View style={[s.planItem, isSelected && s.planItemSelected]}>
         {/*
          Gold left border indicator – visible only when the row is selected.
          This uses the `&&` conditional rendering: if isSelected is false,
          nothing is rendered (no empty View).
        */}
        {isSelected && <View style={s.planItemBorder} />}

        <View style={s.planItemContent}>
          {/* Top row: book name + (optional) today dot */}
          <View style={s.planItemRow}>
            <Text
              style={[s.planItemBook, isSelected && s.planItemBookSelected]}
              numberOfLines={1}
            >
              {book}
            </Text>
            {/*
              &&  conditional rendering:
              isTodayItem && <View> renders today's dot (a small gold circle) ONLY if
              this row is today's actual date.
              If false, React renders nothing (no empty View).
            */}
            {isTodayItem && <View style={s.todayDot} />}
          </View>

          {/* Bottom row: chapter:verse portion */}
          <Text style={s.planItemCV}>{cv}</Text>
        </View>
      </View>
    </TouchableHighlight>
  );
}

// ══════════════════════════════════════════════════════════════════════════════
// RIGHT COLUMN — VERSE DETAIL
//
// The VerseDetail component occupies the main (flex 1) area of the Verse tab.
// It displays the selected day's verse and provides navigation controls.
//
// This component is intentionally a ScrollView rather than a plain View so
// that users can pull‑to‑refresh (the RefreshControl triggers a re‑fetch
// from both offline KJV and online API).
//
// It receives the full `useVerse` hook result, giving it access to:
//   - verse.verse:  the current VerseResult (ref, text, source)
//   - verse.date:   the currently selected date (Date object)
//   - verse.setDate:  changes the selected date, triggering a new verse lookup
//   - verse.loading, verse.source, verse.refresh, etc.
//
// Key UX details:
//   - The "Today" badge appears only when the selected date equals the
//     actual calendar day.
//   - The source badge is hidden while the source is "loading".
//   - The Prev / Next buttons shift the date by one day; the Today button
//     is disabled (and visually muted) when already on today.
//   - A small "↓ Pull to refresh" hint at the bottom educates users about
//     the pull‑to‑refresh gesture.
// ══════════════════════════════════════════════════════════════════════════════

function VerseDetail({ verse }: { verse: ReturnType<typeof useVerse> }) {
  // --------------------------------------------------------------------------
  // Derived date strings
  // --------------------------------------------------------------------------
  // We convert the hook's date to an ISO‑like "YYYY-MM-DD" string so we
  // can compare it to today's date.
  const dateStr  = verse.date.toISOString().slice(0, 10);
  const todayStr = new Date().toISOString().slice(0, 10);

  // isCurrentToday is true only when the user is viewing today's verse.
  // This controls the "Today" badge vs. date label, and disables the
  // Today button.
  const isCurrentToday = dateStr === todayStr;

  // --------------------------------------------------------------------------
  // Render
  // --------------------------------------------------------------------------
  return (
    /*
      ScrollView gives a scrollable container so that even on small
      screens the entire content is reachable. More importantly, its
      `refreshControl` prop enables the pull‑to‑refresh gesture.

      refreshControl props:
        - refreshing:   boolean from useVerse (loading state) – when true,
                        the native spinner is shown.
        - onRefresh:    the callback to trigger a full refresh.
        - tintColor:    (iOS) colour of the spinner.
        - colors:       (Android) array of spinner colours (only first used).

        s is our styles object — the one we have created with StyleSheet.create(...)
    */
    <ScrollView
      style={s.main}
      contentContainerStyle={s.mainContent}
      refreshControl={
        <RefreshControl
          refreshing={verse.loading}
          onRefresh={verse.refresh}
          tintColor="#c9a84c"
          colors={["#c9a84c"]}   // Android spinner colour
        />
      }
      // Hide vertical scrollbar to keep the UI clean.
      showsVerticalScrollIndicator={false}
    >
      {/* ── Reference row ────────────────────────────────────────────────── */}
      <View style={s.refRow}>
        {/* The reference text (e.g. "John 3:16"). If no verse is loaded,
            show an em‑dash as a placeholder. The nullish coalescing
            operator (??) provides the fallback. */}
        <Text style={s.refText}>
          {verse.verse?.ref ?? "—"}
        </Text>
         {/*
          Conditional rendering with the ternary ? : operator.
          If isCurrentToday is true → show a "Today" badge.
          Otherwise → show a short date string (e.g. "5 Mar").
        */}
        {isCurrentToday ? (
          <View style={s.todayBadge}>
            <Text style={s.todayBadgeText}>Today</Text>
          </View>
        ) : (
          <Text style={s.dateLabel}>
            {formatDate(dateStr, "short")}
          </Text>
        )}
      </View>

      {/* ── Source badge ──────────────────────────────────────────────────── */}
      {/*
        Only show the source badge when the source is not "loading".
        While loading, the badge is hidden to avoid flickering.
        The && operator: if the left side is false, nothing is rendered.
      */}
      {verse.source !== "loading" && (
        <View style={[
          s.sourceBadge,
          // Select badge style based on online vs offline.
          verse.source === "online" ? s.sourceBadgeOnline : s.sourceBadgeOffline,
        ]}>
          <Text style={s.sourceBadgeText}>
            {verse.source === "online" ? "🌐 Online" : "📖 KJV"}
          </Text>
        </View>
      )}

      {/* ── Horizontal divider ─────────────────────────────────────────────── */}
      <View style={s.hdivider} />

      {/* ── Verse text ────────────────────────────────────────────────────── */}
      {/*
        If a verse object exists (offline or online), display its text.
        Otherwise show a muted "Loading verse…" message. This handles the
        rare case where the verse object is null but we're not in the
        top‑level loading spinner (e.g., a lookup failed silently).
      */}
      {verse.verse ? (
        <Text style={s.verseText}>
          {verse.verse.text}
        </Text>
      ) : (
        <Text style={s.muted}>Loading verse…</Text>
      )}

      {/* ── Attribution ───────────────────────────────────────────────────── */}
      {/*
        VerseResult.source is a short attribution string, e.g.
        "KJV (offline)" or "OurManna API". Show it if it exists.
        && conditional: renders nothing when the string is null/undefined.
      */}
      {verse.verse?.source ? (
        <Text style={s.attribution}>{verse.verse.source}</Text>
      ) : null}

      {/* ── Navigation buttons ───────────────────────────────────────────── */}
      <View style={s.nav}>
        {/* Prev: subtract 1 day from the current date */}
        <TouchableOpacity
          style={s.navBtn}
          onPress={() => {
            const prev = new Date(verse.date);
            prev.setDate(prev.getDate() - 1);
            verse.setDate(prev);
          }}
        >
          <Text style={s.navBtnText}>◀  Prev</Text>
        </TouchableOpacity>

        {/*
          Today button:
          - Disabled when already viewing today (disabled={isCurrentToday}).
          - Visually dimmed with `navBtnCenterDisabled` style.
          - On press, resets the date to the current system date.
        */}
        <TouchableOpacity
          style={[s.navBtnCenter, isCurrentToday && s.navBtnCenterDisabled]}
          onPress={() => verse.setDate(new Date())}
          disabled={isCurrentToday}
        >
          <Text style={[
            s.navBtnCenterText,
            isCurrentToday && s.navBtnCenterTextDisabled,
          ]}>
            Today
          </Text>
        </TouchableOpacity>

         {/* Next: add 1 day to the current date */}
        <TouchableOpacity
          style={s.navBtn}
          onPress={() => {
            const next = new Date(verse.date);
            next.setDate(next.getDate() + 1);
            verse.setDate(next);
          }}
        >
          <Text style={s.navBtnText}>Next  ▶</Text>
        </TouchableOpacity>
      </View>

      {/* ── Pull‑to‑refresh hint ─────────────────────────────────────────── */}
      {/*
        A small hint to teach the user about the pull‑to‑refresh gesture.
        This is especially useful because the gesture isn't always obvious
        on a content‑only screen. In a production app you might show this
        only once and then dismiss it.
      */}
      <Text style={s.pullHint}>↓ Pull to refresh</Text>

    </ScrollView>
  );
}


// ══════════════════════════════════════════════════════════════════════════════
// STYLES
// ══════════════════════════════════════════════════════════════════════════════

const s = StyleSheet.create({

  root: {
    flex:            1,
    flexDirection:   "row",
    backgroundColor: C.bg,
  },

  divider: {
    width:           1,
    backgroundColor: C.border,
  },

  // ── Sidebar ───────────────────────────────────────────────────────────────────
  sidebar: {
    backgroundColor: C.panel,
  },
  sidebarHeader: {
    flexDirection:     "row",
    justifyContent:    "space-between",
    alignItems:        "center",
    paddingHorizontal: 12,
    paddingVertical:   10,
    borderBottomWidth: 1,
    borderBottomColor: C.border,
  },
  sidebarLabel: {
    fontSize:      10,
    fontWeight:    "700",
    color:         C.faint,
    textTransform: "uppercase",
    letterSpacing: 0.8,
  },
  sidebarCount: {
    fontSize:          10,
    color:             C.accent,
    backgroundColor:   "#1a2a3a",
    paddingHorizontal: 5,
    paddingVertical:   1,
    borderRadius:      8,
    overflow:          "hidden",
  },
  sidebarEmpty: {
    padding:        20,
    alignItems:     "center",
  },
  sidebarEmptyText: {
    fontSize: 12,
    color:    C.dim,
  },

  // Item separator — rendered BETWEEN items, not after the last one
  separator: {
    height:          1,
    backgroundColor: "#1e2338",
    marginLeft:      12,
  },

  // ── Plan list item ─────────────────────────────────────────────────────────────
  planItem: {
    flexDirection:   "row",
    alignItems:      "center",
    height:          ITEM_HEIGHT,
    paddingLeft:     12,
    paddingRight:    8,
  },
  planItemSelected: {
    backgroundColor: "#222840",
  },
  planItemBorder: {
    position:        "absolute",
    left:            0,
    top:             0,
    bottom:          0,
    width:           2,
    backgroundColor: C.gold,
  },
  planItemContent: {
    flex: 1,
  },
  planItemRow: {
    flexDirection: "row",
    alignItems:    "center",
    gap:           5,
  },
  planItemBook: {
    fontSize:   12,
    color:      C.muted,
    flex:       1,
  },
  planItemBookSelected: {
    color:      C.text,
    fontWeight: "600",
  },
  planItemCV: {
    fontSize:   10,
    color:      C.faint,
    fontFamily: "monospace",
    marginTop:  1,
  },
  todayDot: {
    width:           6,
    height:          6,
    borderRadius:    3,
    backgroundColor: C.gold,
    flexShrink:      0,
  },

  // ── Main verse panel ───────────────────────────────────────────────────────────
  main: {
    flex: 1,
  },
  mainContent: {
    padding:       20,
    paddingBottom: 40,
  },

  // Reference row
  refRow: {
    flexDirection:  "row",
    alignItems:     "flex-start",
    justifyContent: "space-between",
    marginBottom:   10,
    gap:            10,
  },
  refText: {
    flex:       1,
    fontSize:   22,
    fontWeight: "bold",
    color:      C.gold,
    lineHeight: 28,
  },
  todayBadge: {
    backgroundColor:   "#2a2010",
    borderRadius:      5,
    paddingHorizontal: 8,
    paddingVertical:   3,
    borderWidth:       1,
    borderColor:       C.gold,
    flexShrink:        0,
    marginTop:         3,
  },
  todayBadgeText: {
    fontSize:   11,
    color:      C.gold,
    fontWeight: "600",
  },
  dateLabel: {
    fontSize:   11,
    color:      C.faint,
    fontFamily: "monospace",
    marginTop:  3,
    flexShrink: 0,
  },

  // Source badge
  sourceBadge: {
    alignSelf:         "flex-start",
    borderRadius:      12,
    paddingVertical:   4,
    paddingHorizontal: 10,
    marginBottom:      12,
    borderWidth:       1,
  },
  sourceBadgeOnline:  { backgroundColor: "#0f2a1a", borderColor: "#166534" },
  sourceBadgeOffline: { backgroundColor: "#1a2438", borderColor: "#1e3a5f" },
  sourceBadgeText: {
    fontSize:   11,
    fontWeight: "600",
    color:      C.text,
  },

  // Horizontal divider
  hdivider: {
    height:          1,
    backgroundColor: C.border,
    marginBottom:    16,
  },

  // Verse text
  verseText: {
    fontSize:    16,
    color:       C.text,
    lineHeight:  26,
    marginBottom: 14,
  },
  attribution: {
    fontSize:     11,
    color:        C.accent,
    marginBottom: 24,
  },
  muted: {
    color:    C.muted,
    fontSize: 14,
  },

  // Navigation
  nav: {
    flexDirection:  "row",
    alignItems:     "center",
    gap:            8,
    marginBottom:   16,
  },
  navBtn: {
    backgroundColor: C.accent,
    borderRadius:    8,
    paddingVertical:  9,
    paddingHorizontal: 14,
  },
  navBtnText: {
    color:      "#fff",
    fontSize:   13,
    fontWeight: "600",
  },
  navBtnCenter: {
    flex:              1,
    backgroundColor:   "#222840",
    borderRadius:      8,
    paddingVertical:   9,
    alignItems:        "center",
    borderWidth:       1,
    borderColor:       C.border,
  },
  navBtnCenterDisabled: {
    backgroundColor: "#1a1f2e",
    borderColor:     "#2d3550",
  },
  navBtnCenterText: {
    color:      C.muted,
    fontSize:   13,
    fontWeight: "600",
  },
  navBtnCenterTextDisabled: {
    color: C.dim,
  },

  pullHint: {
    fontSize:   10,
    color:      C.dim,
    textAlign:  "center",
    fontStyle:  "italic",
  },
});

Step 3 — Run and test

  1. npx expo start --clear
  2. Navigate to the 📖 Verse tab.
  3. Today’s verse: Appears immediately (no spinner for ≥ 2nd launch). Left list: Scrolls to today’s reference automatically. Today dot: Gold dot next to today’s reference. “Today” badge in right panel. “📖 KJV” initially, upgrades to “🌐 Online” with WiFi…
Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.