Le pitch
Since. est une PWA minimaliste pour tracker le temps — jours depuis un événement passé, ou jours jusqu’à un événement à venir. Pas de compte, pas de cloud, pas de pub. Les données restent sur l’appareil, l’app fonctionne offline.
Essayer : presque.cool/since/
Pourquoi ce projet
J’ai une très mauvaise mémoire des dates. Plus largement, j’ai du mal à me projeter intellectuellement dans le temps — passé comme futur. “Ça fait combien de temps qu’on se connait ?” “Le voyage c’est dans combien de jours ?” Ces questions simples m’échappent.
J’ai cherché une app. J’en ai trouvé des dizaines — chargées de features, demandant un compte, affichant des pubs, synchronisant sur un cloud dont je n’ai pas besoin. Tout ce que je voulais : un compteur de jours. Simple. Privé. Offline.
Alors je l’ai fait.
Ce qui le rend unique
La simplicité comme contrainte de design. Pas de social, pas de jalons, pas de gamification, pas de streaks. Juste le temps qui passe, affiché clairement.
- Mode since/until : l’app détecte automatiquement si la date est passée ou future
- Récurrence : anniversaires, events hebdomadaires, rendez-vous mensuels — tout se suit automatiquement
- Personnalisation légère : 60+ icônes et couleurs pour distinguer visuellement chaque événement
- Données locales : tout reste sur l’appareil, exportable en JSON si besoin
L’interface est volontairement sobre. Un fond qui respire doucement, des transitions fluides entre les vues, un grand compteur de jours au centre. Rien de plus.
IndexedDB : le choix de la persistance
localStorage aurait suffi pour quelques événements sérialisés en JSON. Mais je voulais quelque chose de plus robuste — une vraie base de données locale, capable de gérer des migrations si le schéma évoluait.
IndexedDB a la réputation d’être pénible. L’API est callback-based, verbeuse, avec des transactions qu’il faut ouvrir et fermer manuellement. Mon approche : un wrapper async/await minimaliste qui expose trois méthodes (getAll, put, delete) et encapsule toute la mécanique transactionnelle. Le store Zustand n’a aucune idée qu’il parle à IndexedDB — il voit juste des promesses.
Le piège classique : oublier qu’IndexedDB est asynchrone au démarrage. L’app doit gérer le cas où les données ne sont pas encore chargées quand React rend le premier frame. J’utilise un état loading dans Zustand qui bloque l’affichage jusqu’à la fin de la rehydratation.
View Transitions : le polish qui change tout
En v2.0, j’ai ajouté les View Transitions API. L’animation était simple : quand on cliquait sur un événement dans la liste, son icône “morphait” fluidement de sa version compacte (48px dans la carte) à sa version agrandie (120px dans le détail). Le navigateur interpolait automatiquement la position et la taille.
L’implémentation tenait en quelques lignes — un view-transition-name unique par événement, et un document.startViewTransition() au changement de vue. Mais l’effet perceptif était disproportionné : l’app passait de “liste → page” à “un objet qui se transforme”. C’était la différence entre une navigation et une interaction.
Le fallback était transparent : si le navigateur ne supportait pas l’API, la navigation restait instantanée sans animation. Pas de dégradation visible.
En v2.9, ces transitions ont été retirées avec le passage à Preact. Les vues changent maintenant d’un coup.
Les calculs de récurrence
Le cas le plus vicieux : les anniversaires. Si mon anniversaire est le 15 mars et qu’on est le 20 mars, l’app doit afficher “il y a 5 jours” (passé récent) plutôt que “dans 360 jours” (prochain anniversaire). Mais si on est le 10 décembre, elle doit afficher “dans 95 jours” (prochain) plutôt que “il y a 270 jours” (dernier).
L’heuristique : calculer les deux dates candidates (dernière occurrence et prochaine occurrence), puis choisir la plus proche temporellement. Ça semble évident, mais les événements hebdomadaires et mensuels ajoutent des cas limites — un événement “tous les lundis” doit afficher “il y a 2 jours” le mercredi, pas “dans 5 jours”.
Stack technique
- Preact + TypeScript
- Vite comme bundler (avec SWC)
- Zustand pour le state management — persistance localStorage (settings) + IndexedDB (events)
- Tailwind CSS (CSS-first, tokens via variables)
- date-fns pour tous les calculs de dates
- PWA — installable, offline, badge dynamique
- Font Awesome Pro pour la bibliothèque d'icônes
- CSP — Content-Security-Policy avec hashes auto-générés
Évolutions
| Date | Version | Description |
|---|---|---|
| Juin 2021 | 1.0 | Version initiale — Compteur de jours, mode since/until, customisation couleur/icône |
| Oct 2025 | 2.0 | View Transitions API, Notifications Web, Badge API, Export/Import JSON, récurrence |
| Oct 2026 | 2.9 | Un premier chargement allégé, de 106 Ko à 45 Ko de JavaScript, grâce au passage à Preact. Le fondu entre les vues, fait avec les View Transitions, disparaît : les vues changent d’un coup. |
Bilan
Durée : 1-2 jours de développement initial. Maintenance ponctuelle sur 4 ans.
Ce que j’ai appris :
- Que les apps les plus utiles naissent d’un irritant personnel — Since. est l’outil que j’utilise le plus parmi tous mes projets, parce qu’il résout un vrai problème quotidien
- Que le wrapper async/await autour d’IndexedDB transforme une API pénible en quelque chose de transparent — trois méthodes suffisent pour couvrir tous les cas
- Qu’en v2.0, View Transitions API était le meilleur ratio effort/impact que j’aie rencontré — quelques lignes de code, et l’app passait de “fonctionnelle” à “soignée”. Retirée en v2.9
Ce que je referais pareil :
- Since est l’app que j’ouvre le plus souvent parmi tout ce que j’ai fait — précisément parce que je l’ai construite pour moi, sans me demander si quelqu’un d’autre en aurait besoin.
- Garder le scope minimal : un compteur de jours, point. Pas de social, pas de gamification, pas de streaks.
Ce que je changerais :
- Explorer les push notifications pour de vrais rappels (actuellement limité aux notifications à l’ouverture de l’app)
- Ajouter un export/import chiffré pour synchroniser entre appareils sans passer par un cloud
The pitch
Since. is a minimalist PWA for tracking time — days since a past event, or days until a future one. No account, no cloud, no ads. Data stays on your device, the app works offline.
Try it: presque.cool/since/
Why This Project
I have a terrible memory for dates. More broadly, I struggle to project myself mentally in time — past or future. “How long have we known each other?” “How many days until the trip?” These simple questions escape me.
I looked for an app. Found dozens — bloated with features, requiring accounts, showing ads, syncing to clouds I don’t need. All I wanted: a day counter. Simple. Private. Offline.
So I built it.
What Makes It Unique
Simplicity as a design constraint. No social features, no milestones, no gamification, no streaks. Just time passing, clearly displayed.
- Since/until mode: the app automatically detects if the date is past or future
- Recurrence: birthdays, weekly events, monthly appointments — everything tracks automatically
- Light customization: 60+ icons and colors to visually distinguish each event
- Local data: everything stays on device, exportable to JSON if needed
The interface is intentionally minimal. A subtly animated background, smooth transitions between views, a large day counter at the center. Nothing more.
IndexedDB: the persistence choice
localStorage would have been enough for a few events serialized as JSON. But I wanted something more robust — a real local database, capable of handling migrations if the schema evolved.
IndexedDB has a reputation for being painful. The API is callback-based, verbose, with transactions that must be opened and closed manually. My approach: a minimalist async/await wrapper exposing three methods (getAll, put, delete) that encapsulates all the transactional machinery. The Zustand store has no idea it’s talking to IndexedDB — it just sees promises.
The classic trap: forgetting that IndexedDB is asynchronous at startup. The app must handle the case where data isn’t loaded yet when React renders the first frame. I use a loading state in Zustand that blocks rendering until rehydration completes.
View Transitions: the polish that changes everything
In v2.0, I added the View Transitions API. The animation was simple: when you clicked an event in the list, its icon smoothly “morphed” from its compact version (48px in the card) to its enlarged version (120px in the detail). The browser automatically interpolated position and size.
The implementation fit in a few lines — a unique view-transition-name per event, and a document.startViewTransition() on view change. But the perceptual effect was disproportionate: the app went from “list → page” to “an object that transforms”. It was the difference between a navigation and an interaction.
The fallback was transparent: if the browser didn’t support the API, navigation remained instant without animation. No visible degradation.
In v2.9, these transitions were removed with the move to Preact. Views now change at once.
Recurrence calculations
The trickiest case: birthdays. If my birthday is March 15th and today is March 20th, the app should show “5 days ago” (recent past) rather than “in 360 days” (next birthday). But if today is December 10th, it should show “in 95 days” (upcoming) rather than “270 days ago” (last one).
The heuristic: calculate both candidate dates (last occurrence and next occurrence), then pick the temporally closest one. It sounds obvious, but weekly and monthly events add edge cases — a “every Monday” event should show “2 days ago” on Wednesday, not “in 5 days”.
Tech Stack
- Preact + TypeScript
- Vite as bundler (with SWC)
- Zustand for state management — localStorage (settings) + IndexedDB (events) persistence
- Tailwind CSS (CSS-first, tokens via variables)
- date-fns for all date calculations
- PWA — installable, offline, dynamic badge
- Font Awesome Pro for the icon library
- CSP — Content-Security-Policy with auto-generated hashes
Evolution
| Date | Version | Description |
|---|---|---|
| June 2021 | 1.0 | Initial version — Day counter, since/until mode, color/icon customization |
| Oct 2025 | 2.0 | View Transitions API, Web Notifications, Badge API, Export/Import JSON, recurrence |
| Oct 2026 | 2.9 | A lighter first load, from 106 KB to 45 KB of JavaScript, thanks to the move to Preact. The cross-fade between views, built with View Transitions, is gone: views change at once. |
Retrospective
Duration: 1-2 days of initial development. Occasional maintenance over 4 years.
What I learned:
- That the most useful apps come from personal frustrations — Since. is the tool I use most among all my projects, because it solves a real daily problem
- That an async/await wrapper around IndexedDB transforms a painful API into something transparent — three methods cover all use cases
- That in v2.0, the View Transitions API had the best effort/impact ratio I’ve encountered — a few lines of code, and the app went from “functional” to “polished”. Removed in v2.9
What I’d do the same:
- Since is the app I open most among everything I’ve built — precisely because I built it for myself, without wondering whether anyone else would need it.
- Keep the scope minimal: a day counter, period. No social features, no gamification, no streaks.
What I’d change:
- Explore push notifications for real reminders (currently limited to notifications when opening the app)
- Add encrypted export/import to sync between devices without going through a cloud