JustToThePoint English Website Version
JustToThePoint en español

Diary App. Implementing Markdown Preview

The excitement of learning separates youth from old age. As long as you’re learning, you’re not old, Rosalyn S.Yalow.

alt

Understanding Markdown Preview in a Diary App

This code adds a Preview mode to the diary app. Currently, when users write diary entries, they see raw Markdown symbols like # and ** mixed with their text. This new feature lets them switch between seeing the “code” version (Edit mode) and the “formatted” version (Preview mode) - like seeing a Word document as plain text versus seeing it with all the formatting applied.

What is Markdown?

Think of Markdown as a simple way to format text using special symbols:

# Heading becomes a large, bold heading
**bold** becomes bold text
- item becomes a bullet point list
*italic* becomes slanted text

It’s like a secret code that gets translated into pretty formatting. The app shows this code when you’re editing, but shows the pretty version when you preview.

The Two-Part Installation Process

# Installing marked.
# This is the "translator" that converts Markdown code into HTML (the language web pages use for formatting)
npm install marked
#  This is like a dictionary that helps the TypeScript programming language understand marked.
npm install --save-dev @types/marked
npx expo start --clear

The code creates a new file called src/utils/markdownUtils.ts that acts like a recipe card for converting Markdown to HTML.

// src/utils/markdownUtils.ts
//
// Converts Markdown text to a styled HTML string for react-native-render-html.

import { marked } from "marked";
// Setting Up the Translator. The first lines configure marked with two settings:
// gfm: true - GitHub Flavored Markdown, which adds extra features like tables and task lists.
// breaks: true - Makes single line breaks actually create new lines (normally Markdown ignores single line breaks).
marked.setOptions({ gfm: true, breaks: true });

// The CSS Styling Section
// This is a long section that defines how each formatted element should look.
// Body styling: Sets the default text appearance. Uses the system font (looks native on any device)
// 15 pixels tall text (comfortable reading size). More space between lines (1.65 times normal) for readability.
// Light gray color that works on dark backgrounds. Transparent background (so the app's dark theme shows through).
// Headings (h1, h2, h3): The largest heading (#) is 22 pixels, gold-colored
// Medium heading (##) is 18 pixels, also gold. Small heading (###) is 16 pixels, blue
// This creates a visual hierarchy - like chapters, sections, and subsections in a book
// Paragraphs: Space after each paragraph (12 pixels). This prevents text from looking cramped
// Bold and Italic: Bold text is the same color as normal text but thicker
// Italic text is slightly gray and slanted
// Lists: Adds proper spacing and indentation. List items have small gaps between them for readability
// Blockquotes: A gold vertical line on the left side. Gray, indented text.
// Horizontal rules: Creates a crisp, 1-pixel solid line using a dark grayish-blue color (#2d3550)
// that creates visual separation between sections.
// Links: A soft, medium-light blue text to indicate clickable elements.
const CSS = `
body{font-family:-apple-system,sans-serif;font-size:15px;line-height:1.65;
  color:#dde4f0;background:transparent;margin:0;padding:0;}
h1{font-size:22px;color:#c9a84c;margin:0 0 10px;}
h2{font-size:18px;color:#c9a84c;margin:14px 0 8px;}
h3{font-size:16px;color:#5b8fd4;margin:12px 0 6px;}
p{margin:0 0 12px;}
strong{color:#dde4f0;font-weight:700;}
em{color:#9ca3af;font-style:italic;}
ul,ol{margin:0 0 12px;padding-left:20px;}
li{color:#dde4f0;margin-bottom:4px;}
blockquote{border-left:3px solid #c9a84c;margin:0 0 12px;
  padding-left:14px;color:#9ca3af;}
code{font-family:monospace;font-size:13px;background:#222840;
  color:#86efac;padding:1px 5px;border-radius:3px;}
pre{background:#222840;border-radius:6px;padding:12px;}
pre code{background:none;padding:0;}
hr{border:none;border-top:1px solid #2d3550;margin:16px 0;}
a{color:#5b8fd4;}
`;

/**
 * Convert a Markdown string to a fully styled HTML document.
 * Pass the result directly to <RenderHtml source={{ html: ... }} />.
 */
export function markdownToHtml(md: string): string {
  // When the entry is empty:
  if (!md.trim()) {
    // Instead of showing a blank screen, it shows a helpful message
    return `<html><body><p style="color:#4b5563;font-style:italic;">
      Nothing to preview yet. Write something in Edit mode.
    </p></body></html>`;
  }
  // When the entry has content:
  // 1. It takes the Markdown text and converts it using marked
  const body = marked(md) as string;
  // 2. Wraps the result in a complete HTML document structure
  // Includes the CSS styling we defined earlier
  // The result is ready to be displayed by the HTML renderer
  return `<!DOCTYPE html><html><head><meta charset="utf-8">
<style>${CSS}</style></head><body>${body}</body></html>`;
}

Step 3 — Complete app/(tabs)/diary.tsx

This is the main diary screen - the complete page that brings together everything we’ve learned about. Think of it as the “command center” for the diary app. It’s like opening a notebook where you see your list of entries on the left and your writing area on the right.

The Layout Structure

The screen is divided into three main parts:

  1. EntryList - Shows all your diary entries (like a table of contents)
  2. A visual divider - A thin line separating the list from the editor
  3. EditorPanel - Where you write and preview your entries
// app/(tabs)/diary.tsx
//
// DIARY TAB — Full editor + Markdown preview
//
// ─── React & hooks ────────────────────────────────────────────────────────────
// The code imports three essential tools from React:
// useState - For remembering things (like whether you're editing or previewing)
// useRef - For directly accessing elements
// React - The core library itself
import React, { useState, useRef } from "react";

// ─── React Native ─────────────────────────────────────────────────────────────
import {
  View, //  A basic container (like a box)
  Text, // For displaying words
  TextInput, // Where users type
  ScrollView, // Allows scrolling when content is too long
  TouchableOpacity, // Makes things clickable
  StyleSheet, // For styling
  KeyboardAvoidingView, // Prevents the keyboard from covering the input
  Platform, // Detects if it's iOS or Android
  ActivityIndicator, // Shows a loading spinner
  Alert, // Shows popup messages
  useWindowDimensions, // Gets screen size
} from "react-native";

// ─── Third-party ──────────────────────────────────────────────────────────────
// RenderHtml - The tool that displays formatted HTML (for preview mode)
import RenderHtml from "react-native-render-html";

// ─── App code ─────────────────────────────────────────────────────────────────
// useDiary - The custom hook that manages all diary data
import { useDiary } from "../../src/hooks/useDiary";
import type { SaveStatus } from "../../src/hooks/useDiary";
// markdownToHtml - Converts Markdown to HTML
import { markdownToHtml } from "../../src/utils/markdownUtils";
// isToday, formatDate - Helper functions for working with dates
import { isToday, formatDate } from "../../src/utils/dateUtils";
import type { DiaryEntrySummary } from "../../src/db/types";

// ── Colour constants ──────────────────────────────────────────────────────────
// The code defines a color palette that creates the app's visual theme.
// Notice the colors are all dark-themed with some accent colors:
const C = {
  bg:      "#1a1f2e",
  panel:   "#1e2338",
  panel2:  "#222840",
  border:  "#2d3550",
  accent:  "#5b8fd4",
  text:    "#dde4f0",
  muted:   "#9ca3af",
  faint:   "#6b7280",
  gold:    "#c9a84c",
  dim:     "#4b5563",
  red:     "#f87171",
  green:   "#86efac",
};


// ══════════════════════════════════════════════════════════════════════════════
// ROOT SCREEN
// ══════════════════════════════════════════════════════════════════════════════
// The Main Component: DiaryScreen
// This is the parent component that orchestrates everything. It:
export default function DiaryScreen() {
  // 1. Initializes the diary functionality.
  const diary = useDiary();
  // 2. Determines screen dimensions.
  // The code uses useWindowDimensions() to get the screen width. This is important because:
  // (i) Phones can be held in portrait or landscape; (ii) Different devices have different screen sizes
  // (iii) Tablets have much larger screens than phones
  const { width: screenWidth } = useWindowDimensions();
  // 3. Calculates how wide the sidebar should be.
  // (i) Takes 32% of the screen width (about one-third) 3.i =  Math.floor(screenWidth * 0.32)
  // (ii) Caps it at 180 pixels maximum = Math.min(180, 3.i)
  // This prevents the sidebar from becoming too wide on tablets
  const sidebarWidth = Math.min(180, Math.floor(screenWidth * 0.32));

  // 4. Arranges the three main parts of the screen
  return (
    // The KeyboardAvoidingView is a special container that handles a common mobile app problem:
    // the keyboard covering the text input.
    // When users tap to type, the virtual keyboard appears and takes up about half the screen.
    // Without this component, the keyboard would cover the bottom half of the text input,
    // making it impossible to see what you're typing.
    <KeyboardAvoidingView
      style={s.root}
      behavior={Platform.OS === "ios" ? "padding" : "height"}
      keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 0}
    >
      // The root screen uses a horizontal layout (side-by-side) with:
      // A. Left side: The entry list (like chapters in a book)
      <EntryList diary={diary} sidebarWidth={sidebarWidth} />
      // B. Middle: A thin divider line
      <View style={s.divider} />
      // C. Right side: The editor panel (where you write)
      <EditorPanel diary={diary} sidebarWidth={sidebarWidth} />
    </KeyboardAvoidingView>
  );
}

This is different from many mobile apps that use vertical stacking, but it makes sense for tablets or larger phones where you want to see your entries while writing.

The useDiary() hook creates a central data store that both child components use:

This is basically like having a shared notebook that both the table of contents and the writing pages reference.

This section covers the left column - the sidebar that displays all diary entries. Instead of one giant component, we now have:

  1. EntryList - The main sidebar container that manages the overall layout
  2. EntryListItem - A single entry row within the list
// ══════════════════════════════════════════════════════════════════════════════
// LEFT COLUMN — ENTRY LIST
// ══════════════════════════════════════════════════════════════════════════════
// This function accepts props (properties/arguments to a function) - data passed from a parent component.
function EntryList({
  diary,
  sidebarWidth,
}: {
  // typeof useDiary - Gets the type of the useDiary function itself.
  // ReturnType<...> - A TypeScript utility that extracts the type that the function returns.
  // In summary, the diary field must have exactly the same shape as whatever useDiary() return
  diary:        ReturnType<typeof useDiary>;
  // The parent component determines how wide the sidebar should be.
  // This makes EntryList flexible - it can work at any width the parent chooses.
  sidebarWidth: number;
}) {
  return (
    // The outer container is a View (an invisible box).
    // The style uses an array to combine two styles:
    // (i) s.sidebar - The base styles for the sidebar;
    // (ii) { width: sidebarWidth } - An inline style that sets the width dynamically.
    // This pattern of combining a static style with a dynamic one is very common in React Native.
    // The static style handles things that never change,
    // while the inline style overrides or adds values that vary.
    <View style={[s.sidebar, { width: sidebarWidth }]}>
      // The Sidebar Header. This creates a header row with two text elements:
      // a label to the left and the count to the right.
      <View style={s.sidebarHeader}>
        <Text style={s.sidebarLabel}>Journal</Text>
        <Text style={s.sidebarCount}>{diary.entries.length}</Text>
      </View>

      // The Loading Conditional
      // This is a ternary operator for conditional rendering.
      // It reads: "If diary.loading is true, show the spinner. Otherwise, show the scrollable list."
      {diary.loading ? (
        // When loading is true: A small spinner appears, colored gold.
        <View style={s.sidebarLoading}>
          <ActivityIndicator size="small" color={C.gold} />
        </View>
      ) : (
        // When loading is false: A ScrollView contains the list.
        // The inline style { flex: 1 } tells the ScrollView to take up
        // all available remaining space in the sidebar.
        // showsVerticalScrollIndicator={false} hides the scrollbar.
        // This gives a cleaner look. Users can still scroll with touch gestures
        // - they just don't see a visual scrollbar on the side.
        <ScrollView style={{ flex: 1 }} showsVerticalScrollIndicator={false}>
          // Inside the ScrollView, there's another conditional: Is the entries array empty?
          {diary.entries.length === 0 ? (
            // If empty: Show a friendly message guiding the user.
            // The text contains \n characters that insert line breaks.
            // This creates a multi-line message that's visually centered.
            // This is the first-run experience - what a brand-new user sees before they've written anything.
            // Good apps always have helpful empty states rather than showing blank screens.
            <View style={s.emptyEntries}>
              <Text style={s.emptyEntriesText}>
                No entries yet.{"\n"}Tap + New Entry{"\n"}to begin.
              </Text>
            </View>
          ) : (
            // If not empty: Render the list of entries using .map().
            // The .map() method iterates over every entry in the array and creates an EntryListItem component for each one.
            // This is the standard React pattern for rendering lists.
            diary.entries.map((entry: DiaryEntrySummary) => (
              <EntryListItem
                // React requires a unique key for each item in a list. Using entry.date works
                // because dates are unique - you can't have two entries for the same day.
                // The key helps React efficiently update the list when items are added or removed.
                // Without keys, React would have to re-render everything; with keys,
                // it can just update the items that actually changed.
                key={entry.date}
                // Passing data to the child component: entry={entry} - Passes the full entry object
                entry={entry}
                // Compares this entry's date to the currently open entry's date.
                // If they match, this entry is "selected" and will be highlighted.
                isSelected={entry.date === diary.currentDate}
                // Creates a function that, when called, opens this specific entry.
                // The arrow function captures the current entry's date.
                onPress={() => diary.openEntry(entry.date)}
              />
            ))
          )}
        </ScrollView>
      )}

      // The New Entry Button
      // This button sits at the bottom of the sidebar, below the ScrollView.
      // It's always visible regardless of whether the list is empty or full.
      // Notice this doesn't use an arrow function (onPress={diary.newEntry}).
      // That's because newEntry doesn't need any arguments.
      // When pressed, this triggers the newEntry function in the hook, which:
      // Saves any unsaved changes to the current entry; Opens today's entry (or creates a new template).
      <TouchableOpacity style={s.newEntryBtn} onPress={diary.newEntry}>
        <Text style={s.newEntryBtnText}>+ New Entry</Text>
      </TouchableOpacity>
    </View>
  );
}

function EntryListItem({
  entry, isSelected, onPress,
}: {
  // This component receives three props:
  // entry: A DiaryEntrySummary object containing the entry's metadata (date, word count, preview text)
  entry:      DiaryEntrySummary;
  // isSelected: A boolean indicating whether this entry is currently open in the editor
  isSelected: boolean;
  // onPress: A function to call when the item is tapped.
  // The type () => void means "a function that takes no arguments and returns nothing."
  onPress:    () => void;
}) {
  return (
    // The TouchableOpacity Container
    // When isSelected is true, the style array becomes [s.entryItem, s.entryItemSelected].
    // When false, the && operator returns false, and the array becomes [s.entryItem, false].
    // React Native ignores non-style values in style arrays, so a false value is effectively skipped.
    // This means selected items get both the base style and the "selected" style,
    // while unselected items only get the base style
    <TouchableOpacity
      style={[s.entryItem, isSelected && s.entryItemSelected]}
      onPress={onPress}
      // When the user touches this item, it becomes 70% opaque (30% transparent).
      // This provides visual feedback that the touch was registered.
      activeOpacity={0.7}
    >
      // The Selection Border
      // When this entry is selected, a thin accent bar appears on the left edge of the item.
      // This is a common design pattern - a colored vertical bar indicating "this is the current item."
      // Think of it like a bookmark sticking out from the edge of a book.
      {isSelected && <View style={s.entryItemBorder} />}
      // The Date Row.
      // This creates a horizontal row containing the date and possibly a "today" indicator.
      <View style={s.entryDateRow}>
        // Displays the entry's date, with conditional styling.
        <Text style={[s.entryDate, isSelected && s.entryDateSelected]}>
          {entry.date}
        </Text>
        // isToday is a utility function that checks if the entry's date matches today's date.
        // If this entry is today's entry, a small dot appears next to the date.
        // This is a visual indicator that says "this is today's entry."
        // When you have entries spanning months, it's helpful to quickly identify which entry is today's.
        {isToday(entry.date) && <View style={s.todayDot} />}
      </View>
      // The Word Count. This displays the word count for each entry, such as "247 words" or "1,023 words".
      // It gives users a quick sense of how long each entry is.
      <Text style={s.entryMeta}>{entry.wordCount} words</Text>
      // The Preview Text
      // The conditional check: entry.preview ? ... : null checks if a preview exists.
      // Some entries might have empty previews (if they're empty templates that haven't been written in yet).
      // In that case, we don't want to render anything.
      {entry.preview ? (
        // numberOfLines={2}: This limits the preview text to a maximum of two lines.
        // If the preview is longer, it gets truncated with an ellipsis (...).
        // This keeps list items compact and uniform, preventing very long previews from taking up too much space.
        <Text style={s.entryPreview} numberOfLines={2}>{entry.preview}</Text>
      ) : null}
    </TouchableOpacity>
  );
}

When the parent DiaryScreen renders: <EntryList diary={diary} sidebarWidth={280} />

  1. EntryList receives the hook’s return value and a width
  2. It renders a sidebar with a header, scrollable content area, and button
  3. Inside the scrollable area, it maps over diary.entries: diary.entries.map((entry: DiaryEntrySummary) => ...
  4. For each entry, it creates an EntryListItem with the entry data and selection state
  5. When the user taps an item, onPress fires, calling diary.openEntry(entry.date) (onPress={() => diary.openEntry(entry.date)}).
  6. The hook updates diary.currentDate, triggering a re-render
  7. The re-render causes isSelected to be recalculated for each item
  8. The previously selected item loses its highlight, the new one gains it

Detailed Explanation of the Editor Panel Component

This is the right column of the diary screen - the main editor area where users write and preview their entries.

// ══════════════════════════════════════════════════════════════════════════════
// RIGHT COLUMN — EDITOR + PREVIEW
// ══════════════════════════════════════════════════════════════════════════════

function EditorPanel({
  diary,
  sidebarWidth,
}: {
  // This component receives two props:
  // (1) diary: The hook's return value, same as EntryList
  diary:        ReturnType<typeof useDiary>;
  // (ii) sidebarWidth: The width of the left sidebar, needed for calculating preview width
  // The parent component passes these values when rendering <EditorPanel diary={diary} sidebarWidth={280} />.
  sidebarWidth: number;
}) {
  // Creating a Reference to the TextInput component.
  // A ref is like a "box" that can hold a reference to something in the UI. Unlike state,
  // changing a ref doesn't trigger a re-render. Refs are used when you need to directly interact with a component.
  const inputRef = useRef<TextInput>(null);

  // The Mode State
  // It tracks whether the user is:
  // "edit" mode - seeing and editing the raw Markdown text
  // "preview" mode - seeing the formatted, rendered version
  // The useState return gives us: mode - the current value; setMode - a function to change it.
  const [mode, setMode] = useState<"edit" | "preview">("edit");

  // Getting Screen Dimensions
  // useWindowDimensions is a React Native hook that returns the current dimensions of the device's screen/window.
  // It automatically updates when the screen rotates or when the app window resizes
  // The syntax { width: screenWidth } extracts just the width property and renames it to screenWidth.
  const { width: screenWidth } = useWindowDimensions();
  // Calculating Preview Width. This is a mathematical calculation that determines how much horizontal space the preview has.
  // screenWidth (the total width of the device screen) minus sidebar (the width of the left sidebar),
  // divider (the width of a divider/border between the sidebar and editor), padding (32, total horizontal padding).
  // The RenderHtml component (used later for preview) requires an explicit contentWidth property.
  const contentWidth = screenWidth - sidebarWidth - 1 - 32;

  // The Switch to Preview Function
  // This is the function called when the user taps the "Preview" button.
  async function switchToPreview() {
    // The preview needs to render the latest content.
    // If there are unsaved changes (isDirty is true), we save them first.
    // The async keyword means this function returns a promise.
    // The await pauses execution until saveEntry() completes.
    if (diary.isDirty) await diary.saveEntry();
    // The setMode("preview") at the end changes the mode, which causes
    // the component to re-render and show the preview instead of the editor.
    setMode("preview");
  }

  // ── Early Return: No Entry Open ──────────────────────────────────────────────────────────
  // If no entry is open (currentDate is null), the component returns early with a completely different layout.
  // If currentDate is null, we show the welcome screen and exit. The welcome screen contains:
  if (!diary.currentDate) {
    return (
      <View style={s.noEntry}>
        // A notebook emoji (📔) as a friendly visual
        <Text style={s.noEntryIcon}>📔</Text>
        // A title: "Your Prayer Diary"
        <Text style={s.noEntryTitle}>Your Prayer Diary</Text>
        // Descriptive text: "Write freely — your words are private, stored only on this device."
        // The privacy message is important for a diary app.
        // Users need reassurance that their personal reflections aren't being uploaded anywhere.
        <Text style={s.noEntryText}>
          Write freely — your words are private,{"\n"}
          stored only on this device.
        </Text>
        // A button to create today's entry
        <TouchableOpacity style={s.noEntryBtn} onPress={diary.newEntry}>
          <Text style={s.noEntryBtnText}>+ New Entry for Today</Text>
        </TouchableOpacity>
      </View>
    );
  }

The Delete Confirmation Function

What is Alert.alert? This is a React Native API that shows a native dialog box. On iOS, it appears as a modal popup. On Android, it appears as a dialog with buttons. It’s used for important confirmations and notifications.

The three arguments:

  1. Title: “Delete this entry?” - A concise question that grabs our users attention
  2. Message: Uses a template literal (backticks) to include the current date. If currentDate is “2026-08-06”, the message becomes: “Permanently delete the entry for 2026-08-06. Cannot be undone.”
  3. Button array: An array of button configurations. Each button has:
    text - The label shown on the button, e.g., “Cancel”, “Delete”.
    style - The button’s visual style, e.g., “cancel” (subtle, non-emphasized), “destructive” (typically red on iOS, indicating a dangerous action).
    onPress - The function to call when pressed, e.g., No onPress (tapping it just dismisses the alert), (1) setMode("edit") -Switch back to edit mode (in case we were in preview)- and (2) diary.deleteEntry(diary.currentDate!) -Actually delete the entry-.

The non-null assertion (!): diary.currentDate! tells TypeScript I know currentDate might be null according to its type, but I guarantee it's not null here. This is safe because we’re inside the section that only renders when currentDate is truthy (the early return above would have caught the null case).

Why confirm before deleting? Deleting a diary entry is permanent. There’s no “undo” and no trash folder. The confirmation dialog prevents accidental deletions. This is a best practice for any destructive action.

  function confirmDelete() {
    Alert.alert(
      "Delete this entry?",
      `Permanently delete the entry for ${diary.currentDate}. Cannot be undone.`,
      [
        { text: "Cancel", style: "cancel" },
        {
          text: "Delete", style: "destructive",
          onPress: () => { setMode("edit"); diary.deleteEntry(diary.currentDate!); },
        },
      ]
    );
  }

The Toolbar Layout

The toolbar is organized into four sections, separated by comments:

  1. Left section: Date label and “Today” badge
  2. Middle section: Save status indicator
  3. Mode toggle: Edit/Preview buttons
  4. Action buttons: Save and Delete (only visible in edit mode)
  // Formatting the Date Label
  // This calls a utility function formatDate that converts the ISO date string into a more human-readable format.
  // Input: "2026-08-06" (ISO format)
  // Output: "August 6, 2026" or "Thursday, August 6, 2026"
  const dateLabel = formatDate(diary.currentDate, "long");

  return (
    <View style={s.editor}>

      {/* ── Toolbar ─────────────────────────────────────────────────────────── */}
      <View style={s.toolbar}>

        {/* 1. Date + Today badge */}
        <View style={s.toolbarLeft}>
          // The date text shows the formatted date. The numberOfLines={1} prevents the date
          // from wrapping to multiple lines if it's too long.
          // If the date is wider than available space, it gets truncated with an ellipsis.
          <Text style={s.toolbarDate} numberOfLines={1}>{dateLabel}</Text>
          // The Today badge: The isToday() utility function checks if currentDate matches today's actual date.
          // **If it does**, a small badge appears next to the date saying "Today."
          // When you're browsing old entries, it's easy to lose track of which one is today's.
          // The badge gives immediate visual feedback.
          {isToday(diary.currentDate) && (
            <View style={s.todayBadge}>
              <Text style={s.todayBadgeText}>Today</Text>
            </View>
          )}
        </View>

        {/* 2. Auto-save status */}
        // This is a separate component that displays the current save status.
        <SaveStatusBadge status={diary.saveStatus} isDirty={diary.isDirty} />

        {/* 3. The Mode Toggle: Edit / Preview */}
        // This creates a toggle switch with two buttons side by side: "Edit" and "Preview."
        <View style={s.modeToggle}>
          <TouchableOpacity
            // When mode is "edit", the style becomes [s.modeBtn, s.modeBtnActive] - the active style applies
            // When mode is "preview", the style becomes [s.modeBtn, false] - only the base style applies
            style={[s.modeBtn, mode === "edit" && s.modeBtnActive]}
            // The Edit button uses onPress={() => setMode("edit")} - switching to edit mode
            // doesn't need to save because you're just editing what's already there.
            onPress={() => setMode("edit")}
          >
            // Same pattern for the text: when active, the text gets a special "active" style
            <Text style={[s.modeBtnText, mode === "edit" && s.modeBtnTextActive]}>
              Edit
            </Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={[s.modeBtn, mode === "preview" && s.modeBtnActive]}
            // The Preview button uses onPress={switchToPreview} - this DOES save first, as we discussed earlier.
            onPress={switchToPreview}
          >
            <Text style={[s.modeBtnText, mode === "preview" && s.modeBtnTextActive]}>
              Preview
            </Text>
          </TouchableOpacity>
        </View>

        {/* 4. Save and Delete Buttons (Edit Mode Only) */}
        // The mode === "edit" && wrapper ensures these buttons only appear in edit mode.
        // In preview mode, there's nothing to save or delete - you're just viewing.
        // This reduces clutter and prevents confusion.
        {mode === "edit" && (
          // The fragment <>... is a React fragment.
          // It groups multiple elements without adding an extra wrapper View.
          <>
            // 4.A The Save button
            <TouchableOpacity
              // The style array has three elements: (i) s.toolbarBtn - Base button style;
              // (ii) s.toolbarBtnSave - Save-specific style;
              // (iii) !diary.isDirty && s.toolbarBtnDisabled - Applied only when not dirty
              style={[s.toolbarBtn, s.toolbarBtnSave,
                      !diary.isDirty && s.toolbarBtnDisabled]}
              onPress={diary.saveEntry}
              // The disabled prop prevents pressing when: !diary.isDirty - Not dirty - (nothing to save) or
              // currently saving (avoid double-save)
              disabled={!diary.isDirty || diary.saving}
            >
              // The button label changes dynamically:
              // When diary.saving is true, it shows "Saving…" (indicating in-progress work)
              // Otherwise, it shows "Save"
              <Text style={[s.toolbarBtnText,
                            !diary.isDirty && s.toolbarBtnTextDisabled]}>
                {diary.saving ? "Saving…" : "Save"}
              </Text>
            </TouchableOpacity>
            // 4.B The Delete button
            // Always enabled (you can delete an entry even if it's clean).
            <TouchableOpacity
              // It has a distinct "delete" style.
              style={[s.toolbarBtn, s.toolbarBtnDelete]}
              // Uses confirmDelete instead of directly deleting
              onPress={confirmDelete}
            >
              <Text style={s.toolbarBtnDeleteText}>Delete</Text>
            </TouchableOpacity>
          
        )}

      </View>

The Hint Bar

This is a contextual hint that changes based on the current state. It uses nested ternaries - a ternary inside a ternary.

      {/* ── Hint bar ─────────────────────────────────────────────────────────── */}
      <View style={s.hintBar}>
        <Text style={s.hintText}>
          // When in preview mode:
          // "Preview · tap Edit to resume writing"
          {mode === "preview"
            ? "Preview · tap Edit to resume writing"
            // When in edit mode and dirty (unsaved changes):
            // "● Unsaved · auto-saves in 2 seconds"
            : diary.isDirty
              ? "● Unsaved · auto-saves in 2 seconds"
              // When in edit mode and clean:
              // "Markdown: # Heading bold italic - list"
              // This shows formatting hints - teaching the user how to use Markdown syntax.
              : "Markdown: # Heading  **bold**  *italic*  - list"}
        </Text>
      </View>

Why show formatting hints? Markdown isn’t intuitive for everyone. By showing quick examples, the app educates users about available formatting options. The hint is subtle and non-intrusive.

The Edit Mode TextInput

      {/* ── Content: Edit OR Preview ──────────────────────────────────────────
        Conditional rendering (not display:none) so the TextInput is fully unmounted in Preview mode
        — it would otherwise still capture keys.
        In web development, you might hide an element with display: none while keeping it in the DOM.
        But in React Native, this doesn't work well for TextInputs - a hidden TextInput
        might still capture keyboard input or cause other issues. By unmounting (removing from the component tree),
        we ensure the TextInput is completely gone in preview mode.
      */}

      {mode === "edit" ? (
        // The Edit Mode TextInput
        <TextInput
          // Attaches the ref we created earlier, allowing programmatic access to the TextInput
          ref={inputRef}
          // Makes the editor fill remaining space with padding and readable font size
          style={s.textInput}
          // 🧩 **Controlled component**: the displayed text always equals the hook's content state
          // A controlled component is one where the source of truth is your React state, not the UI element itself.
          // The text shown inside the editor is always whatever diary.content contains.
          // The UI cannot change itself — it must wait for React state to update.
          // If diary.content changes for any reason (loading a saved entry, clearing the editor, undo, etc.),
          // the TextInput updates automatically.
          value={diary.content}
          // Every keystroke triggers our state setter (diary.setContent). React updates diary.content.
          // The TextInput re-renders with the new text.
          onChangeText={diary.setContent}
          // Allows multiple lines (like a document, not a form field).
          multiline={true}
          // Text starts at the top of the field (important on Android).
          textAlignVertical="top"
          // Multi-line placeholder showing what the user can do. It includes example Markdown syntax.
          placeholder={
            "Begin your prayer reflection...\n\n" +
            "# Heading\n**bold**  *italic*\n- list item\n> blockquote"
          }
          // Muted color for placeholder text.
          placeholderTextColor={C.dim}
          // Enables autocorrect (helpful for writing).
          autoCorrect={true}
          // Capitalizes first letter of each sentence.
          autoCapitalize="sentences"
          // Enables spell checking (on platforms that support it).
          spellCheck={true}
          // Allows scrolling within the text input when content exceeds screen.
          scrollEnabled={true}
          // Uses the dark keyboard theme (matching the app's dark theme).
          keyboardAppearance="dark"
          // The return key on the keyboard shows "return" (not "go" or "send").
          returnKeyType="default"
          // Pressing return doesn't dismiss the keyboard (allows multi-line editing with enter key)
          blurOnSubmit={false}
        />

      ) : (
        // The Preview Mode
        // When in preview mode, we show the formatted version of the user's Markdown content.
        // The ScrollView wrapper allows scrolling through long content.
        <ScrollView
          style={s.previewScroll}
          contentContainerStyle={s.previewContent} // it adds padding inside the scroll area
        >
          // This is a third-party library that renders HTML in React Native.
          // React Native can't display HTML directly, so this component converts HTML tags into native components.
          <RenderHtml
            // contentWidth - The width calculated earlier. Required by RenderHtml to properly layout content.
            contentWidth={contentWidth}
            // This is a two-step process:
            // 1. markdownToHtml(diary.content) - Converts Markdown text to HTML string
            // 2. Passes that HTML to RenderHtml for display
            source={{ html: markdownToHtml(diary.content) }}
            // Custom styles for each HTML tag.
            tagsStyles={{
              // h1: Gold color, 22pt font, bold, margin below
              h1:         { color: C.gold,   fontSize: 22, fontWeight: "700",
                            marginBottom: 8 },
              // Gold color, 18pt font, bold, spacing above and below
              h2:         { color: C.gold,   fontSize: 18, fontWeight: "700",
                            marginTop: 14, marginBottom: 6 },
              h3:         { color: C.accent, fontSize: 16, fontWeight: "600",
                            marginTop: 12, marginBottom: 4 },
              p:          { color: C.text,   marginBottom: 12 },
              strong:     { color: C.text,   fontWeight: "700" },
              em:         { color: C.muted },
              li:         { color: C.text,   marginBottom: 4 },
              blockquote: {
                borderLeftWidth: 3, borderLeftColor: C.gold,
                paddingLeft: 14,    color: C.muted, marginLeft: 0,
              },
              code: {
                fontFamily:      Platform.OS === "ios" ? "Courier New" : "monospace",
                fontSize:        13,
                backgroundColor: C.panel2,
                color:           C.green,
              },
              a: { color: C.accent },
            }}
            // baseStyle - The default style applied to all content: normal text color, 15pt font, 24pt line height.
            baseStyle={{ color: C.text, fontSize: 15, lineHeight: 24 }}
          />
        </ScrollView>

      )}

    </View>
  );
}

How the Component Flows

  1. Component mounts with mode set to “edit”
  2. No entry open: Shows welcome screen, early returns
  3. Entry opens: Toolbar appears with date, status, toggle, and action buttons
  4. User types: Content updates, auto-save timer starts, “unsaved” badge appears
  5. User taps “Preview”: Saves if dirty, switches to preview mode
  6. Preview shows: Markdown converted to HTML, rendered with styled formatting
  7. User taps “Edit”: Returns to edit mode, TextInput remounts with content
  8. User taps “Delete”: Confirmation dialog appears, deletes if confirmed

The SaveStatusBadge Component

This is a small, focused component that displays the current save status of the diary. It’s an excellent example of how to create a reusable UI element that handles multiple visual states cleanly.

Think of this as a traffic light for saving:

// ══════════════════════════════════════════════════════════════════════════════
// SAVE STATUS BADGE
// ══════════════════════════════════════════════════════════════════════════════

function SaveStatusBadge({
  status, isDirty,
}: {
  // This component receives two props (properties):
  // 1. status: The current save status: actively saving, just saved, failed
  // The type SaveStatus is a union type defined earlier in the hook file as "idle" | "saving" | "saved" | "error".
  status:  SaveStatus;
  // 2. isDirty: A boolean (true/false) indicating whether there are unsaved changes.
  isDirty: boolean;
}) {
  // The Early Return
  // If nothing is happening (no save operation in progress or recently completed)
  // AND there are no unsaved changes, don't show anything.
  if (status === "idle" && !isDirty) return null;

  // The Configuration Object
  // What is a Record? In TypeScript, Record<K, V> is a utility type that creates an object type where:
  // K (first parameter) is the type of the keys. The object's keys must be of type SaveStatus ("idle", "saving", "saved", "error").
  // V (second parameter) is the type of the values. Each value must be an object with three properties:
  // label: a string; bg: a string (background color); emoji: a string;
  const cfg: Record<SaveStatus, { label: string; bg: string; emoji: string }> = {
    // This is the "nothing to show" state; label: "" - empty string means no text;
    // bg: "transparent" - No background color; emoji: "" - No emoji
    // It's included in the config for completeness (TypeScript requires it), even though
    // the early return prevents it from being used.
    idle:   { label: "",           bg: "transparent", emoji: "" },
    saving: { label: "Saving…",   bg: "#1a2438",     emoji: "💾" },
    saved:  { label: "Saved",     bg: "#0f2a1a",     emoji: "✅" },
    error:  { label: "Error",     bg: "#2a1010",     emoji: "❌" },
  };
  // This is object destructuring.
  // It looks up the current status in the config object and extracts the three properties.
  const { label, bg, emoji } = cfg[status];

  // The Badge Container
  return (
    // This creates the badge's outer container. It uses the style array pattern:
    // s.statusBadge - Base badge style
    // { backgroundColor: bg } - Inline style that sets the background color dynamically based on the current status
    <View style={[s.statusBadge, { backgroundColor: bg }]}>
      // This is a ternary operator for conditional rendering.
      // It checks if the status is "saving" and renders different content accordingly.
      {status === "saving"
        // When status is "saving":
        // The component shows an ActivityIndicator - a spinning animation that indicates work in progress.
        // size="small" - A compact spinner that fits in the badge
        // color={C.accent} - Uses the accent color
        // style={{ marginRight: 4 }} - Adds 4 pixels of space to the right of the spinner, separating it from the text
        ? <ActivityIndicator size="small" color={C.accent} style={{ marginRight: 4 }} />
        // When status is anything else (saved, error):
        // The component shows an emoji:
        // Saved: Shows "✅" (checkmark) - communicates success
        // Error: Shows "❌" (X mark) - communicates failure
        // Why use emojis? They're built into every device, require no custom images, and are universally understood.
        : <Text style={s.statusEmoji}>{emoji}</Text>
      }
      // This conditionally renders the text label.
      // If label has a value (is truthy), it displays the text.
      // If empty (like in the "idle" state), it renders nothing.
      // Why conditionally render? In the "idle" state, the label is empty string "".
      // We don't want to render an empty Text component - it would be wasteful and might create unwanted spacing.
      {label ? <Text style={s.statusLabel}>{label}</Text> : null}
    </View>
  );
}

What does the user see?

// ══════════════════════════════════════════════════════════════════════════════
// 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" },
  sidebarLoading:    { flex: 1, alignItems: "center", justifyContent: "center",
                       padding: 20 },
  emptyEntries:      { padding: 20, alignItems: "center" },
  emptyEntriesText:  { fontSize: 12, color: C.dim, textAlign: "center",
                       lineHeight: 18 },

  entryItem:         { paddingHorizontal: 12, paddingVertical: 10,
                       borderBottomWidth: 1, borderBottomColor: "#1a1f2e" },
  entryItemSelected: { backgroundColor: C.panel2 },
  entryItemBorder:   { position: "absolute", left: 0, top: 0, bottom: 0,
                       width: 2, backgroundColor: C.gold },
  entryDateRow:      { flexDirection: "row", alignItems: "center",
                       gap: 5, marginBottom: 3 },
  entryDate:         { fontSize: 11, color: C.muted, fontFamily: "monospace",
                       fontWeight: "600" },
  entryDateSelected: { color: C.gold },
  todayDot:          { width: 6, height: 6, borderRadius: 3,
                       backgroundColor: C.gold, flexShrink: 0 },
  entryMeta:         { fontSize: 10, color: C.dim, marginBottom: 3 },
  entryPreview:      { fontSize: 11, color: C.faint, lineHeight: 15 },

  newEntryBtn:       { paddingVertical: 12, paddingHorizontal: 12,
                       borderTopWidth: 1, borderTopColor: C.border,
                       alignItems: "center" },
  newEntryBtnText:   { fontSize: 12, color: C.gold, fontWeight: "700" },

  // No entry
  noEntry:           { flex: 1, alignItems: "center", justifyContent: "center",
                       padding: 32, gap: 12 },
  noEntryIcon:       { fontSize: 52 },
  noEntryTitle:      { fontSize: 20, fontWeight: "bold", color: C.muted },
  noEntryText:       { fontSize: 13, color: C.faint, textAlign: "center",
                       lineHeight: 20 },
  noEntryBtn:        { backgroundColor: C.accent, borderRadius: 10,
                       paddingVertical: 12, paddingHorizontal: 28 },
  noEntryBtnText:    { color: "#fff", fontSize: 14, fontWeight: "700" },

  // Editor column
  editor:            { flex: 1, flexDirection: "column", backgroundColor: C.bg },

  // Toolbar
  toolbar:           { flexDirection: "row", alignItems: "center",
                       paddingVertical: 8, paddingHorizontal: 12,
                       backgroundColor: C.panel, borderBottomWidth: 1,
                       borderBottomColor: C.border, gap: 6, flexWrap: "wrap" },
  toolbarLeft:       { flex: 1, flexDirection: "row", alignItems: "center",
                       gap: 6 },
  toolbarDate:       { fontSize: 12, color: C.text, fontWeight: "600", flex: 1 },
  todayBadge:        { backgroundColor: "#2a2010", borderRadius: 4,
                       paddingHorizontal: 5, paddingVertical: 2,
                       borderWidth: 1, borderColor: C.gold, flexShrink: 0 },
  todayBadgeText:    { fontSize: 9, color: C.gold, fontWeight: "700" },

  // Mode toggle (segmented control)
  modeToggle:        { flexDirection: "row", borderRadius: 6, borderWidth: 1,
                       borderColor: C.border, overflow: "hidden", flexShrink: 0 },
  modeBtn:           { paddingHorizontal: 10, paddingVertical: 5,
                       backgroundColor: "transparent" },
  modeBtnActive:     { backgroundColor: C.accent },
  modeBtnText:       { fontSize: 11, color: C.faint, fontWeight: "600" },
  modeBtnTextActive: { color: "#fff" },

  // Toolbar action buttons
  toolbarBtn:            { borderRadius: 6, paddingHorizontal: 10,
                           paddingVertical: 5, flexShrink: 0 },
  toolbarBtnSave:        { backgroundColor: C.accent },
  toolbarBtnDisabled:    { backgroundColor: C.border },
  toolbarBtnText:        { fontSize: 12, color: "#fff", fontWeight: "700" },
  toolbarBtnTextDisabled:{ color: C.faint },
  toolbarBtnDelete:      { backgroundColor: "#2a1010", borderWidth: 1,
                           borderColor: "#4a2020" },
  toolbarBtnDeleteText:  { fontSize: 12, color: C.red },

  // Save status badge
  statusBadge:       { flexDirection: "row", alignItems: "center",
                       borderRadius: 5, paddingHorizontal: 7,
                       paddingVertical: 4, gap: 3, flexShrink: 0 },
  statusEmoji:       { fontSize: 11 },
  statusLabel:       { fontSize: 11, color: C.muted, fontWeight: "600" },

  // Hint bar
  hintBar:           { paddingHorizontal: 12, paddingVertical: 5,
                       borderBottomWidth: 1, borderBottomColor: C.border,
                       backgroundColor: C.panel },
  hintText:          { fontSize: 10, color: C.dim, fontStyle: "italic" },

  // Text editor (Edit mode)
  textInput:         { flex: 1, padding: 16, color: C.text, fontSize: 15,
                       lineHeight: 24, textAlignVertical: "top",
                       fontFamily: Platform.OS === "ios"
                         ? "Courier New" : "monospace" },

  // Markdown preview (Preview mode)
  previewScroll:     { flex: 1 },
  previewContent:    { padding: 16, paddingBottom: 32 },
});

Step 4 — Run and test

  1. npx expo start --clear
  2. Navigate to 📔 Diary → tap + New Entry → the template appears.
  3. Quick test. Type this in the editor, then tap Preview/ Edit (raw Markdown text).
# Morning Prayer

**Lord**, I am grateful today.

## Intentions

- Peace in my family
- Wisdom at work

> Be still and know that I am God.
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.