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, j’ai ajouté les View Transitions API. L’animation est simple : quand on clique sur un événement dans la liste, son icône “morphe” fluidement de sa version compacte (48px dans la carte) à sa version agrandie (120px dans le détail). Le navigateur interpole automatiquement la position et la taille.
L’implémentation tient en quelques lignes — un view-transition-name unique par événement, et un document.startViewTransition() au changement de vue. Mais l’effet perceptif est disproportionné : l’app passe de “liste → page” à “un objet qui se transforme”. C’est la différence entre une navigation et une interaction.
Le fallback est transparent : si le navigateur ne supporte pas l’API, la navigation reste instantanée sans animation. Pas de dégradation visible.
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
- React + 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
- Compression — Brotli + Gzip sur les assets
- 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 |
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
- Que View Transitions API est le meilleur ratio effort/impact que j’ai rencontré — quelques lignes de code, et l’app passe de “fonctionnelle” à “soignée”
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, I added the View Transitions API. The animation is simple: when you click an event in the list, its icon smoothly “morphs” from its compact version (48px in the card) to its enlarged version (120px in the detail). The browser automatically interpolates position and size.
The implementation fits in a few lines — a unique view-transition-name per event, and a document.startViewTransition() on view change. But the perceptual effect is disproportionate: the app goes from “list → page” to “an object that transforms”. It’s the difference between a navigation and an interaction.
The fallback is transparent: if the browser doesn’t support the API, navigation remains instant without animation. No visible degradation.
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
- React + 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
- Compression — Brotli + Gzip on assets
- 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 |
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 View Transitions API has the best effort/impact ratio I’ve encountered — a few lines of code, and the app goes from “functional” to “polished”
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