{"id":13850139,"url":"https://github.com/fpapado/ephemeralnotes","last_synced_at":"2025-04-28T01:32:04.783Z","repository":{"id":37789445,"uuid":"146645139","full_name":"fpapado/ephemeralnotes","owner":"fpapado","description":"Ephemeral is a Progressive Web App for writing down words and their translations, as you encounter them.","archived":false,"fork":false,"pushed_at":"2023-01-07T07:33:57.000Z","size":5107,"stargazers_count":44,"open_issues_count":39,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-05T04:41:17.110Z","etag":null,"topics":["elm","flashcards","journaling","language-learning","offline-first","pwa"],"latest_commit_sha":null,"homepage":"https://ephemeralnotes.app","language":"Elm","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fpapado.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":"isfotis","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2018-08-29T18:58:21.000Z","updated_at":"2024-09-13T18:49:16.000Z","dependencies_parsed_at":"2023-02-06T15:01:29.252Z","dependency_job_id":null,"html_url":"https://github.com/fpapado/ephemeralnotes","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fpapado%2Fephemeralnotes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fpapado%2Fephemeralnotes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fpapado%2Fephemeralnotes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fpapado%2Fephemeralnotes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fpapado","download_url":"https://codeload.github.com/fpapado/ephemeralnotes/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251233936,"owners_count":21556762,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elm","flashcards","journaling","language-learning","offline-first","pwa"],"created_at":"2024-08-04T20:00:59.892Z","updated_at":"2025-04-28T01:32:03.719Z","avatar_url":"https://github.com/fpapado.png","language":"Elm","funding_links":["https://ko-fi.com/isfotis"],"categories":["Elm"],"sub_categories":[],"readme":"# Ephemeral\n\nEphemeral is a progressive web app for writing down words and their translations, as you encounter them.\n\nThe live version is at https://ephemeralnotes.app.\n\nI made this originally when I immigrated to Finland and wanted a way to connect words with events and the world around me. I make this app in my free time.\n\nThe app works offline and all data is stored locally to your device.\n\n![The map screen of the application, with a marker on Helsinki.](docs/map.jpg)\n\n## Table of Contents\n\n1. [Features](#features)\n2. [Principles](#principles)\n3. [Development](#development)\n4. [Production](#production)\n5. [List of Technologies Used](#list-of-technologies-used)\n6. [Architecture](#architecture)\n7. [Contributing](#contributing)\n8. [Code of Conduct](#code-of-conduct)\n9. [License](#license)\n\n## Features\n\n- Allows you to capture notes, time, and (optionally) the location\n- Works offline\n- Is resilient to errors\n- You can install it / add to home screen on most devices (including smartphones, desktops etc.)\n- You can always export and import data, as it's stored locally\n\n## Principles\n\nThe following principles guide the project.\nEach feature or change should be evaluated against them.\n\n### Data ownership\n\nThe application should avoid storing data in a remote location.\nThe user notes (including time and location) should stay on the local device.\nIt is up to the user to export them and share them however they want.\n\nNote that this has implications for the architecture; many server-side solutions are not possible. That fact should be communicated to the user.\n\n### Accessibility\n\nPeople use the web in different ways, and we should accommodate them.\n\nThe Web Content Accessibility Guidelines 2.1 ([WCAG 2.1](https://www.w3.org/TR/WCAG21/)) cover a number of criteria to consider when putting things online. We should strive for level AA, or even AAA.\n\nIn practice, this means evaluating the markup that we put on the page, validating our assumptions about interactions, and ensuring that states are communicated correctly. Where such choices are made, we should document the reasoning and share references.\n\n### Performance\n\nNot everyone has expensive, fast devices.\nIn fact, [as a trend, it appears that computing has gotten cheaper, not faster](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/).\nA median device can still run slow if overloaded with Javascript.\nWe should aim to deliver the experience in the amount of JS needed, and no more.\n\nThis has implications for the choice of technology, feature set and testing.\nFor example, Elm allows us to have very small and perfomant bundles.\nSimilarly, using Web Components and ports for more specialized APIs (maps, storage persistence) allow us to use tested implementations, without rolling our own potentially heavy and unreliable ones.\n\n### Don't give up on the user\n\nAn application that works locally can fail in many ways. We should inform the user why things failed, whether it is expected, and whether they can do anything about it (even if it means trying again later!).\n\nFor example, Geolocation can fail, data can get corrupted, the user might change the contents, or a service worker might be waiting to update. We should be honest about those possibilities, and architect the code in a way that will prompt us to communicate these to the user.\n\n### Document why\n\nThis is partially covered by the above.\nWhere code is concerned, we should strive to document why a certain decision was made.\n\nWas a certain CSS order needed to progressively enhance features? Did we elect a specific markup pattern to expose features to Assistive Technologies? Were there compromises or assumptions in any of them? These are the kind of things we should document.\n\n## Development\n\nTo start, you will need to [install Node.js](https://nodejs.org/en/download/). Node is a runtime for Javascript that works outside of the browser. Node comes with `npm`, which allows us to install packages. Both of them are used to set up development tooling.\n\nThen, in a terminal:\n\n```shell\nnpm ci\nnpm run dev\n```\n\nThis will start a local server at http://localhost:8080.\nYou can visit that page to see the application running.\nIt will reload automatically as you make changes.\n\nIf you want more information on how exactly the application files and assets are built, [check out the docs/build.md](docs/build.md) file.\n\n## Production\n\nFor production, we have to build the application and deploy it to a public location.\n\nBuilding the application is done with:\n\n```shell\nnpm run build\n```\n\nThis places the built files under `dist/`\n\nThe deployment is handled automatically through the [Now for GitHub integration](https://zeit.co/github). It is not something that normally you would need to do if making a Pull Request.\n\nThat said, if you want to run the app in production mode (which applies some optimisations), you can run:\n\n```shell\nnpm run prod\n```\n\nThis will start a server at http://localhost:5000.\n\nFinally, if you need the closest to production parity, which includes routes and cache headers in the server response, you can use [now dev](https://zeit.co/docs/v2/development/basics) via:\n\n```shell\nnpm install -g now\nnow dev\n```\n\nThis can be useful if you want to debug the full server round-trips, as well as the Service Worker caching.\n\n## List of Technologies Used\n\nIf you are interested in contributing, you will see many of the following terms and libraries.\nWe introduce them here to establish a common starting point.\n\n[Elm](https://elm-lang.org/) is a delightful language for reliable web apps.\nWe use it for the core UI and data architecture.\nElm enables us to express our interface as a function of data.\nIt also helps us handle error cases in the code, in a way that is resilient and can be expressed to the user in an actionable way.\n\nA Progressive Web App (PWA) works offline via a Service Worker. A Service Worker is a script that runs in the browser detached from the website, and caches assets and other scripts. A PWA can be installed locally, on most devices. [Here is an intro to PWAs, by Google](https://developers.google.com/web/progressive-web-apps/).\n\nService Workers are a rather low-level API. To make them more declarative, and to manage the asset invalidation when they change, we use [Workbox](https://developers.google.com/web/tools/workbox/).\n\nFor storage, Ephemeral uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), which is a local, persisted database in the browser.\nIndexedDB is built-in to browsers, and exposes a number of low-level interfaces.\nTo make that more manageable, we use [idb](https://github.com/jakearchibald/idb), which is a library that wraps IndexedDB in promises, and tries to expose errors more consistently.\n\nFor maps, we use [Leaflet](https://leafletjs.com/). Leaflet is a well-established JS library for rendering maps and features on them.\n\nIn order to integrate Elm with more complex UI, like the Leaflet map, we use the set of technologies called [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components). They allow us to wrap UI in a way that Elm can render declaratively, but that keep internal details and handlers.\n\nFor parts where we would interface with Javascript, we use [TypeScript](https://www.typescriptlang.org/), a typed superset of Javascript. It offers a type system aimed to model Javascript's behaviour. Keep in mind, the type system is different to Elm's, but nonetheless it helps us type the boundaries between parts of the application.\n\n## Architecture\n\n### The Index\n\nThe application has the entry point `index-dev.ts` or `index-prod.ts`, depending on the environment.\n\nThe index is responsible for importing the parts of the application.\nThey are :\n\n- The Core Elm Application and Ports (imported statically)\n- The Leaflet Web Component (imported dynamically)\n- Information UI components (imported dynamically)\n\n### The Core Elm Application and Ports\n\nThe core Elm application is initialised from `src/client.ts`.\n\nIt does the following:\n\n- Sets up the flags (some initial configuration) for the Elm application\n- Attaches the Elm application to the DOM\n- Sets up listeners for each port; ports are the way in which Elm communicates to JS\n\n#### The Elm Application Internals\n\n**NOTE**: Before you read this section, I recommend [going through the official Elm guide](https://guide.elm-lang.org/architecture/). It establishes a lot of the concepts used below :)\n\nThe Elm application entry point is `src/Main.elm`.\nMain imports each `Page/*.elm` module, and handles the routing to each one.\nEach Page module might have its own internal `model`, `update`, `view` and `subscriptions`.\nMain ensures that each Page's functions are connected correctly, and that they get the data they need.\n\nSome pages might need data from a top-level shared model, such as the list of Entries that the user has stored. We tentatively call this `Context`, but other names and ways to do this, such as [SharedState](https://github.com/ohanhi/elm-shared-state), exist as well.\n\nIf you want to go through the application, my recommendation would be to start the Page module you are interested in, and then work up or down from there. You could also start at Main, but your mileage may vary.\n\nApart from Page modules, the rest is centered around data types.\nFor example, the `Entry.elm` module defines the `Entry` type. An Entry has some internal structure, and some exposed functions to work with it. It also establishes some functions to encode into and decode from JSON. This is useful when we need to communicate with JS through ports, or when we write to a file.\n\n#### Ports\n\nPorts are used to communicate with JS. There are a few things that we handle with port modules:\n\n- Saving and fetching things from the IndexedDB Store\n- Setting user preferences for Dark Mode\n- Geolocation\n- Handling notifications of Service Worker updates and Installation\n\nThese are usually in paired TypeScript and Elm modules. Thus, we have `Store.elm`/`Store.ts`, `DarkMode.elm`/`DarkMode.ts` and so on.\n\nAn important decision here, is that each module has only two ports, `fromElm` and `toElm`.\nThis enforces all the data to pass through a common interface, and makes it clear that there are no request/response relationships between Elm and JS. Something in JS/TS could fail to respond altogether, and we should be prepared for it.\n\nFor each pair, then:\n\n- An Elm module, say `Store.elm`, establishes functions to send messages to JS (`FromElm`). It also establishes messages it can process from JS (`ToElm`).\n- Each Typescript module, say `Store.ts`, then has a `handleSubMessage` function. It can do arbitrary things with the message, such as going to the database, or just logging to the console. It gets passed a `sendToElm` function, that it can use to send a `ToElm` message.\n- On the Elm module, any Page (or Main) that wants to use messages from JS, declares it in the `subscriptions` function.\n\nThis approach has a little bit more typing, and encoding/decoding.\nHowever, given the [Principles](#principles) outlined above, creating a more strict boundary seems important for a resilient application.\n\n### The Leaflet Web Component\n\nThe Leaflet Web Component is one of the more interesting bits of interop here.\nLeaflet is a fantastic library, and it has a heavy OOP and imperative API.\n\nTo add a marker to leaflet, you:\n\n- Create a layer\n- Add the feature to the layer\n- Add the layer to the map, if not already there\n- Bind the popup content of the feature, if any\n\nAlongside that, Leaflet wants undisturbed access to its section of the DOM.\nThis clashes with Elm though, which in our case controls the entire `body`, and would try to diff things. (This is all working as intended btw, I think it makes sense).\n\nWe could try to do all this with ports. In fact, I tried it in the past, but it was messy.\nA different way I wanted to take was to use a Web Component.\n\nIn an ideal world, I'd like to render the following from Elm, and get a Leaflet map:\n\n```html\n\u003cleaflet-map defaultLatitude=\"23.93\" defaultLongitude=\"60.16\"\u003e\n  \u003cleaflet-marker latitude=\"23.93\" longitude=\"60.16\"\u003e\n    \u003cp\u003eHello, I have HTML content in a popup!\u003c/p\u003e\n  \u003c/leaflet-marker\u003e\n\u003c/leaflet-map\u003e\n```\n\nWeb components allow us to get exactly that. We tie the imperative behaviour to changes in the DOM (a process manual, but workable). Then we add the real Leaflet map to an \"internal\" DOM, or Shadow DOM. As far as Elm is concerned it is only diffing the top-level `\u003cleaflet-map\u003e` and `\u003cleaflet-marker\u003e`, unaware of the \"internal\" DOM that Leaflet is happy to make changes to.\n\nFor me, this pairing of Elm (which will respect custom elements) and Web Components is great, especially when it comes to rendering UI! You could also use Web Components for more JS/Elm communication through Custom Events, but I find ports a cleaner boundary for that.\n\nIf you want to see the small mess of setting this up, check out `src/leaflet/leaflet-map.ts`.\n\n## Contributing\n\nIf you are interested in contributing, please [consult CONTRIBUTING.md](/CONTRIBUTING.md) for how to do so. That document covers topics such as opening issues, creating PRs, running tests, as well as the principles and code of conduct.\n\n## Code of Conduct\n\nThis project has a [Code of Conduct](/CODE_OF_CONDUCT.md). By contributing you agree to abide by it.\n\n## License\n\nMozilla Public License Version 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffpapado%2Fephemeralnotes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffpapado%2Fephemeralnotes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffpapado%2Fephemeralnotes/lists"}