Skip to main content

Calculate Shift

Screen 1Screen 1
Screen 2Screen 2
Screen 3Screen 3
Launch App

Calcola Turno

Open Web App ↗

Works offline on all devices

The Project

Calcola Turno is a Progressive Web App (PWA) that solves a real problem for factory workers: knowing in advance which shift they will work (morning, afternoon, or night) on any future day.
Designed to be installed like a native app, it works completely offline and automatically calculates shifts without requiring manual updates or an internet connection.

Source Code

<!-- DESIGN 
------
* This HTML is characterized by:
* - A Single Page Application (SPA) structure managed via visibility states
* (hidden class) rather than multi-page navigation, to ensure instant
* transitions typical of native apps
* - Native System Fonts stack (Segoe UI, San Francisco) instead of external
* imports to maximize load performance and blend in with the OS UI (Metro/iOS)
* - A strict PWA (Progressive Web App) configuration in the <head>,
* specifically targeting iOS quirks (meta apple-mobile-web-app) which
* often ignores standard manifest declarations.
-->

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">

<!--
* I set maximum-scale=1.0 and user-scalable=no to prevent the browser
* from zooming in when double-tapping buttons, mimicking the
* non-scalable interface of a native mobile application
-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<title>Calcola Turno</title>

<link rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.json">

<!--
* Dynamic theme-color: allows the browser status bar to adapt
* automatically to the user's system preference (Light/Dark mode),
* maintaining visual consistency
-->
<meta name="theme-color" content="#0078D4" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">

<!-- --- iOS SPECIFIC CONFIGURATION --- -->

<!--
* iOS Safari requires specific link tags for the home screen icon
* and meta tags to hide the URL bar (standalone mode), as it does
* not fully support the web app manifest for these features yet
-->
<link rel="apple-touch-icon" href="icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Turni">

<!-- * Fallback icon for desktop browser tabs -->
<link rel="icon" type="image/png" href="icon.png">

</head>
<body>

<!--
* This wrapper simulates the device frame during development/desktop view,
* but acts as a transparent container in mobile view thanks to
* CSS media queries
-->
<div class="device-frame">
<div class="device-screen">
<div class="container">

<!-- VIEW 1: ONBOARDING -->
<div id="onboarding-view">
<h1>Calcola turno</h1>
<div class="section">
<div class="section-title">che turno fai questa settimana?</div>

<!--
* I used a flex/grid container for buttons to handle
* responsive alignment. The buttons utilize data-attributes
* for logic to keep the DOM clean from styling classes
-->
<div class="onboarding-buttons">

<!--
* Grid Stack Technique: inside the button, span and loader
* occupy the same grid area to allow seamless transition
* between text and loading spinner without layout shifts
-->
<button class="group-btn" data-select-shift="Mattino">
<span class="btn-text">Mattino</span>
<div class="loader"></div>
</button>

<button class="group-btn" data-select-shift="Pomeriggio">
<span class="btn-text">Pomeriggio</span>
<div class="loader"></div>
</button>

<button class="group-btn" data-select-shift="Notte">
<span class="btn-text">Notte</span>
<div class="loader"></div>
</button>

</div>
</div>
</div>

<!-- VIEW 2: MAIN APPLICATION
Hidden by default, toggled via JavaScript upon initialization
-->
<div id="main-view" class="hidden">
<h1>Calcola turno</h1>

<!-- Information Card Component -->
<div class="section">
<div class="current-week-card">
<div class="card-label">Questa settimana sei di</div>
<div class="card-value" id="week-shift-display">-</div>
</div>
</div>

<!--
* Calendar Component
* Divided into Controls (Nav), Header (Weekdays) and Grid (Days)
-->
<div class="section">
<div class="calendar-controls">
<div class="calendar-header">
<button class="nav-btn" id="prev-month" aria-label="Mese precedente"></button>

<!--
* Hybrid Input Wrapper: overlays a native transparent
* date input over a custom text div. This forces mobile
* browsers to open their native date scrollers (wheel),
* improving UX over a custom-built JS picker
-->
<div class="month-picker-wrapper">
<div class="month-year" id="month-year"></div>
<input type="month" id="native-month-picker" aria-label="Seleziona mese">
</div>

<button class="nav-btn" id="next-month" aria-label="Mese successivo"></button>
</div>

<button id="today-btn" class="today-btn">Torna a oggi</button>
</div>

<div class="weekdays">
<div class="weekday">lun</div>
<div class="weekday">mar</div>
<div class="weekday">mer</div>
<div class="weekday">gio</div>
<div class="weekday">ven</div>
<div class="weekday">sab</div>
<div class="weekday">dom</div>
</div>

<!-- Grid container populated dynamically by JavaScript -->
<div class="days" id="calendar-days"></div>
</div>

<!-- Result Feedback Area -->
<div class="result" id="result">
<div class="result-date" id="result-date">oggi</div>
<div class="result-shift" id="result-shift">-</div>
</div>

<div class="footer-actions">
<button id="reset-btn" class="text-btn">Non è corretto? Reimposta</button>
</div>
</div>

</div>
</div>
</div>

<!--
* Install Banner Component
* Positioned fixed at the bottom, separate from the main flow
* Visibility is managed by localStorage and device type detection in JS
-->
<div id="install-banner" class="install-banner hidden">
<div class="install-content">
<img src="icon.png" alt="Icona" class="install-icon">
<div class="install-text">
<span class="install-title">Installa l'App</span>
<span class="install-desc" id="install-instructions">Per un'esperienza migliore</span>
</div>
<button id="install-btn-action" class="install-action-btn">Installa</button>
<button id="close-install" class="install-close" aria-label="Chiudi banner"></button>
</div>
</div>

<div id="update-toast" class="update-toast hidden">
<div class="update-content">
<span class="update-text">È disponibile un aggiornamento</span>
<button id="refresh-btn" class="update-action-btn">AGGIORNA</button>
</div>
</div>

<script src="script.js"></script>
</body>
</html>

Second Version

This is the second version of Calcola Turno. I had built the first one a year and a half ago, long before starting this journey, using AI exclusively in “Vibe Coding” mode (a term that didn’t even exist “back then”) without really understanding what was happening under the hood. It took me several days, I don’t remember exactly how many, but I had no skills, only the desire to build it at any cost.
It worked: the app spread around the factory and solved a real problem for people working on 3 shifts.
This time it’s different. I rebuilt the app from scratch in a total of 5 hours (2 hours for the working core + 3 hours for polish and responsiveness), applying everything I’ve learned in the last few months.

Calcola Turno Version 1.0

Version 1.0 (2024)

Why Rebuild It?

The night before going back to work I was anxious. I didn’t know if I would stay in the same department, and just the thought of not being able to keep learning and dedicating myself to projects was “killing” me. I calmed that anxiety by doing the only thing I truly love: building something. I picked up the Calcola Turno idea again, but this time with a completely different level of awareness. I wasn’t just “making an app work” with the help of AI. I was orchestrating the AI, telling it exactly what I needed, asking for confirmation on every choice, and checking that I wasn’t missing anything important.

The Design System: A Reimagined Windows Phone 8

I used the design system created for Telephone Number Validator, heavily inspired by Windows Phone 8, but with one particular choice: no primary color in the traditional sense.
Since it’s an app meant for the factory workers where I work, I knew the Windows aesthetic would feel familiar: all company computers run Windows, so the Metro/Modern UI visual language is immediate and recognizable for them.

I wanted the user to have only 3 colors in mind while using the app:

  • Light Blue = Morning
  • Yellow = Afternoon
  • Purple = Night

It’s a colorful app, but with neutral primary colors. Paradoxical, but it works: here colors don’t serve as decoration, they act as direct information.

Icon Design: From the Calendar to the Three Shifts

Mentally, I was stuck on the idea of a calendar with a clock inside it, which defined the very first version.
I wanted something different, and by looking at other apps on my phone I realized something: the best icons are the ones with absolute simplicity. No complex overlays, no details that get lost at 60×60 pixels.

I made several versions in Figma, iterating continuously: a calendar made of 3 puzzle pieces representing the 3 shifts, then a clock with an arrow rotating around it. None of them fully convinced me.
I reopened the app and found the answer right in front of me: the 3 onboarding rectangles, the ones that represent the 3 shifts (Morning, Afternoon, Night).

That guaranteed three fundamental things:

  • Visual consistency with the app’s internal interface
  • Immediate recognizability for users
  • Absolute visual cleanliness, with no redundant elements

The icon of the very first version was completely different, it was more detailed, but (maybe) less effective. This time I chose the essential. After all, the app promises to do just one thing: tell you which of the 3 shifts you’ll work.

Old Icon 2024

2024

New Icon 2025

2025

Architecture: Offline-First and Cache Management

From the beginning I thought about the architecture and what I would need for the backend, in particular localStorage and cache management. I wanted it to work offline and remember the shift chosen by the user during onboarding. I wanted it working that same evening, so I orchestrated the AI by sharing my ideas and asking it to confirm whether I was missing something or if there were better ways to reach the solution.

The midnight problem
An interesting point was the algorithm to automatically switch the calendar day at exactly midnight.
At first I had an approach I thought was elegant: a Timer that calculated exactly how much time was left until midnight. When opening the app, JavaScript would check how long remained before midnight and set itself up accordingly for the day change.

The problem? I would have run into iOS and Android behavior where they freeze browsers in the background to save battery. The timer would stop.
I went with a truly simple, almost trivial solution: a 60-second Interval. Every 60 seconds JavaScript checks, put simply, whether today != yesterday (today is different from yesterday).

I cared about battery impact and found it to be extremely low. That’s because the CPU wakes up for a micro-instant, compares a number, sees nothing changed, and goes “back to sleep.” It’s far more expensive for the battery to handle graphical rendering or a network connection.
So the “precise midnight timer” idea would have risked bugs where the app doesn’t update if the phone goes into standby, while the energy savings would be so tiny as to be basically unmeasurable.

And so, in 2 hours I had a working app. I spent another 2 hours the next day polishing details and designing the desktop and smartphone landscape experience.

Responsive Layout: From Mobile to Dashboard

The landscape experience turns the app into a dashboard: shifts are laid out horizontally, making the most of the available space and offering a broader view of the calendar.
It was a super fun project. I stopped because I would have gotten lost in the details.

Reflections: Empathy, Process, and Control

I’m not cynical, otherwise I wouldn’t aim to do this job, but I believe people don’t really care to hear that you can do this or that; they care about what you can actually do in practice and, to be even more specific, what you can concretely do that benefits them.
I think it’s the same theme as public speaking. Carlo Loiudice used to say (I don’t remember whether in one of his courses or in his podcasts) that the audience is selfish: they don’t care who you are, what you’ve done, or why you’re there talking, they care about what they take home from what you say.
That’s exactly why I believe it’s vital that you like what you do not only for the results you get and other people’s feedback, but for the process itself.

The best feeling wasn’t from colleagues who complimented me and added the app to their home screen; it was the feeling that even though I was using AI much more than usual, I knew exactly what we were doing and why.
That’s what allowed me to do better work, far more fun and educational than the previous version, in 1/10 of the time.

What I Learned

Frameworkless SPA architecture:

  • Implemented a pure Single Page Application pattern by toggling visibility (.hidden) between onboarding and the main view, eliminating navigation and page reloads for a “native app” feel.
  • Minimal app-state management with localStorage for user group persistence and global variables (selectedDate, currentMonth) for current UI state.

Deterministic algorithm for cyclical shifts:

  • Built a shift calculation system based on an epoch (January 5, 2025) and a modular 3-week cycle, completely avoiding databases or external APIs.
  • Implemented getWeekNumber() with European normalization (Monday as the start of the week) and Sunday edge-case handling.
  • Fixed a DST (Daylight Saving Time) bug by using Math.round instead of Math.floor to compensate for 167.9 or 168.1-hour “weeks” during clock changes.
  • Reverse lookup pattern: automatic deduction of group A/B/C from the user’s current shift, making onboarding more intuitive (“what shift are you on today?” instead of “are you in group A?”).

PWA and offline-first architecture:

  • Service Worker with a “cache-first, network fallback” strategy and aggressive precaching of all critical assets.
  • Lifecycle handled with skipWaiting() and clients.claim() for immediate updates, but with user-side UI control (toast + manual reload).
  • Split install handling: native beforeinstallprompt for Android/desktop and a fallback with manual instructions for iOS (which ignores part of the standard manifest).
  • Manual cache versioning (turni-app-v10) with automatic cleanup of older versions during activate.

App-like mobile UX:

  • Viewport meta with maximum-scale=1.0 and user-scalable=no to prevent accidental zoom from double-tap.
  • iOS-specific: apple-mobile-web-app-capable, apple-touch-icon, media-query-driven theme-color for a status bar consistent with the theme.
  • Tactile feedback via a .clicked class with intentional delays (setTimeout) to avoid cutting off animations during fast interactions.
  • Native type="month" input kept invisible and layered over a custom label: on mobile it opens the native picker (iOS wheel, Android spinner) instead of a less performant custom HTML datepicker.

Utility-first CSS with responsive variables:

  • Centralized design system in :root for colors, spacing, typography, and component sizes, with overrides via media queries (dark mode, desktop, tablet, landscape).
  • Grid “stack technique” to overlay button text and loader without layout shifts during loading state.
  • Advanced responsiveness: desktop (min-width: 950px) turns the app into a two-column dashboard using display: contents to neutralize semantic wrappers and grid-template-areas to fully reorganize the layout.
  • CSS animations (@keyframes softPivot) retriggered via JavaScript (trick: animation='none' → reflow → restore) for a synchronized restart on state change.

Accessible keyboard navigation:

  • “Day navigation” mode toggled with ArrowDown: arrows navigate day-by-day (±1 or ±7), with automatic month scrolling when crossing boundaries.
  • Clear separation between “month navigation” (ArrowLeft/Right change month) and “day navigation” to avoid input conflicts.
  • Handling for Enter, Tab, and Escape to enter/exit day mode and return to today.

Real-time time handling and lifecycle:

  • Midnight auto-refresh with a 60s check (setInterval) comparing lastKnownDay to the current date.
  • visibilitychange listener to re-check state when the app returns to the foreground after long standby (prevents the “app stayed open all night” bug).
  • DST-safe checks to avoid being off by one day during clock changes.

Performance and thermal management:

  • No external fonts, hidden scrollbar, touch-action: manipulation, -webkit-tap-highlight-color: transparent to reduce rendering overhead and improve tactile feel.
  • Strategic use of pointer-events: none during loading states to prevent double clicks and race conditions.
  • “Silent Mac” approach: the 60s interval has battery impact that’s hard to measure because the CPU wakes only to compare a number, not for rendering or networking.

Advanced UI patterns:

  • Calendar with smart padding: previous/next month days (.other-month) are disabled but visible, completing the 7-column grid without visual gaps.
  • Dynamic classes based on shift (.mattino, .pomeriggio, .notte) applied to both .today and .selected for immediate semantic color-coding.
  • Click animation with a delay before state change (setTimeout 150ms) to deliver full visual feedback before triggering a heavier rerender.

Next:
Right now the priority is learning React. I started today, thanks to the fact that I got quite ahead with university over the last month. But the challenge now isn’t learning a new library, because I’m confident I can do it, just like I managed to learn JavaScript, it’s balancing work, university, and this path I’d like to pursue full-time.