Skip to content

Latest commit

 

History

History

README.md

Notes Offline

Your notes, always available.

An offline-first notes app built with Meteor 3.5. Demonstrates IndexedDB persistence, optimistic UI, cached subscriptions, soft delete, and PWA support using the jam:* package family.

Demo: https://notes-offline.sandbox.galaxycloud.app/

Stack

Runtime Meteor 3.5
Frontend React 19
UI Mantine UI
Offline jam:offline (IndexedDB, auto-sync, cross-tab)
Methods jam:method (optimistic, offline queuing)
Pub/Sub jam:pub-sub (cached subscriptions)
Soft Delete jam:soft-delete
Validation Zod
PWA Workbox (via workbox-webpack-plugin + Rspack)
i18n Lingui (SWC macro + minimal JSON catalogs)
Tests Mocha
Code Quality ESLint (flat config: react, hooks, jsx-a11y, import-x) + Prettier
E2E Playwright
Build Rspack

Features

  • Create, edit, delete notes with auto-save (500ms debounce)
  • Trash with recovery and permanent delete
  • Search notes by title, content, or tags
  • Pin important notes to the top
  • Tags support
  • Markdown editing with live preview
  • Export/import notes as JSON
  • Auto dark mode (follows system preference via Mantine colorScheme="auto")
  • Keyboard shortcuts (Alt+N new note, Esc deselect)
  • Online/offline/syncing status indicator
  • Installable as PWA, works fully offline
  • Multi-language UI (English, Spanish, Portuguese) with per-device preference

Running it

meteor npm install
npm start

Visit http://localhost:3000/.

Command What it does
npm start Start the app
npm test Integration tests (Mocha, watch mode)
npm run test:headless Integration tests (Mocha, headless/CI)
npm run lint Lint and check formatting
npm run lint:fix Lint and auto-fix + format
npm run e2e E2E tests (Playwright, interactive UI)
npm run e2e:headless E2E tests (Playwright, headless)
npm run i18n:extract Extract messages from source to JSON

Before running E2E tests for the first time, install Playwright's browsers with npx playwright install.

How it's structured

imports/
  api/notes/
    collection.js    # Mongo.Collection + jam:soft-delete
    schema.js        # Zod schemas
    methods.js       # jam:method definitions
    publications.js  # Publications
  locales/
    en/messages.json # Source catalog (generated by `lingui extract`)
    es/messages.json # Spanish translations
    pt/messages.json # Portuguese translations
  ui/
    App.jsx          # MantineProvider + AppShell layout
    NotesList.jsx    # Sidebar with search + note cards
    NoteEditor.jsx   # Editor with auto-save
    EmptyState.jsx   # Empty state
    owner.js         # Per-device ownerId helper (localStorage)
    i18n.js          # Lingui bootstrap + activateLocale
client/
  main.jsx           # Entry point
  main.css           # Mantine styles
server/
  main.js            # Import API modules

What makes it offline-first

  • jam:offline stores data in IndexedDB and syncs with the server when connected
  • jam:method queues method calls while offline and replays them on reconnect
  • jam:pub-sub caches subscription data so the app loads instantly from cache
  • jam:soft-delete marks items as deleted instead of removing them, enabling trash and recovery
  • Workbox service worker (configured through Rspack) caches pages and assets for full offline PWA support

Internationalization

Lingui is integrated into the build pipeline in two complementary ways:

  • @lingui/swc-plugin is registered in .swcrc so the macros (t, <Trans>, <Plural>) are transformed by Meteor's built-in SWC loader at compile time. No rspack config changes, no Babel.
  • @lingui/loader compiles the JSON catalogs on the fly during bundling (see the inline-loader import in imports/ui/i18n.js), so there's no separate compile step and no generated .mjs files to commit or ignore.

Catalogs live as minimal flat JSON under imports/locales/{en,es,pt}/messages.json. The initial locale comes from localStorage if set, otherwise from navigator.language, falling back to English. A language dropdown in the sidebar header lets users switch locale, and the choice is persisted to localStorage['notes-offline.locale'].

Macros in use:

// Static JSX strings
<Trans>Your notes, always available</Trans>

// Dynamic labels, placeholders, aria attributes, with interpolation
<TextInput placeholder={t`Search notes...`} />
<ActionIcon aria-label={t`Remove tag ${tag}`} />

// Plural-aware counters, per-locale rules (ICU MessageFormat)
<Plural value={notes.length} one="# note" other="# notes" />

At build time @lingui/swc-plugin rewrites each macro call into a plain i18n._(id) lookup against the compiled catalog. The hashed IDs (FEKiRx, bpgy4J, etc.) you see in imports/locales/{locale}/messages.json are generated from the macro sites.

Workflow to add or change a string:

  1. Edit the source (use <Trans>Hello</Trans> in JSX or t`Hello` in expressions).
  2. Run npm run i18n:extract to refresh the JSON catalogs.
  3. Fill in translations in imports/locales/es/messages.json and imports/locales/pt/messages.json.
  4. No compile step. @lingui/loader picks up the updated JSON on the next bundle.

To add a new locale, append it to locales in lingui.config.js, add it to SUPPORTED_LOCALES in imports/ui/i18n.js, then run npm run i18n:extract.

Deployment

  • Galaxy: meteor deploy your-app.meteorapp.com
    • To try it quickly with a free tier and shared MongoDB: meteor deploy your-app.meteorapp.com --free --mongo
  • Any Node.js host: meteor build gives you a standard Node bundle
  • MUP: automated deploy to your own server over SSH

Links