← Tutte le idee Sport e benessere

Captime

Il timer per gli allenamenti CrossFit e HIIT

Incasso mensile: 481 USD (fonte indicata nella scheda)

Cosa fa

App timer per allenamenti a intervalli (AMRAP, EMOM, Tabata) con impostazioni personalizzate e registrazione video.

Come guadagna

Ha entrate ricorrenti ogni mese, verificate su TrustMRR. Prezzi e piani esatti sono indicati sul sito del progetto.

Rendilo tuo

  • Per le palestre: il timer da proiettare sul grande schermo con il nome della palestra.
  • Allenamenti del giorno condivisi da un coach con i suoi atleti.

Le frasi da scrivere all'AI

1. Base del sito e grafica

Crea un'app web in italiano chiamata "Tempo WOD": un timer per allenamenti a intervalli (AMRAP, EMOM, Tabata, For Time) con impostazioni personalizzate, voce, suoni e lo storico degli allenamenti. Usa React, Vite, TypeScript e Tailwind CSS.

Pagine da creare:
1. Pagina iniziale con i tipi di timer e il prezzo
2. Scelta del timer e impostazioni (durata, intervalli, round)
3. Timer a tutto schermo leggibile a 3 metri
4. Allenamenti salvati e preferiti
5. Storico con risultati (round fatti, tempo)

Stile: da palestra: sfondo nero #000000, numeri enormi bianchi, verde #22C55E per il lavoro, rosso #EF4444 per il riposo, giallo per il conto alla rovescia, carattere "Bebas Neue". Nessuna immagine generica di stock.

Tutto deve funzionare perfettamente da telefono. Tutti i testi in italiano, chiari e senza parole tecniche.

2. Database, account e sicurezza

Collega il progetto a Supabase e attiva la registrazione con email e password.

Crea queste tabelle:
- "allenamenti": nome, tipo di timer, impostazioni in JSON, preferito, collegati all'utente
- "risultati": allenamento, round o tempo ottenuti, note, data

Regole di sicurezza obbligatorie:
- ogni utente vede solo i suoi allenamenti e risultati
- il piano non si modifica dal browser

Usa esattamente il codice SQL che trovi nei "Dettagli tecnici" di questa scheda: eseguilo nel SQL Editor di Supabase prima di continuare, poi collega le pagine alle tabelle.

3. Il motore dei timer

Tutti i timer si basano su un'unica funzione che trasforma le impostazioni in una sequenza di fasi (codice nei Dettagli tecnici):
- AMRAP: un'unica fase di lavoro lunga X minuti;
- EMOM: ogni minuto per N minuti;
- Tabata: 20 secondi di lavoro e 10 di riposo per 8 round (modificabili);
- For Time: cronometro con tempo massimo.

Lo schermo mostra il tempo della fase in numeri enormi, il round, e il colore della fase (verde lavoro, rosso riposo). Conto alla rovescia di 10 secondi prima di partire; suoni a 3-2-1 generati con il Web Audio API; voce che annuncia "Riposo" e "Ultimo round". Lo schermo non si spegne (Wake Lock).

Il tempo si calcola dall'orologio, non contando i secondi, così resta preciso anche se il telefono rallenta.

4. Allenamenti salvati e storico

Si salvano gli allenamenti con un nome ("Fran", "EMOM 12 burpees") e si avviano in un tocco. A fine allenamento si inseriscono i round fatti o il tempo, con note.

Storico con il grafico dei risultati per lo stesso allenamento, per vedere i miglioramenti. Funziona anche senza connessione (PWA).

5. Pagamenti con Stripe

Aggiungi un abbonamento a 1,99 € al mese o 14,99 € all'anno. Gratis l'utente ha: i 4 timer di base senza salvataggio. Con il piano Pro sblocca: allenamenti salvati illimitati, storico con grafici, voce e sequenze personalizzate.

A pagamento riuscito il webhook porta l'utente al piano Pro; se l'abbonamento viene disdetto o non rinnovato lo riporta al piano gratuito.

Per la sessione di pagamento e il webhook segui la guida "Pagamenti con Stripe" di Fattibile.

6. Controlli specifici di questo progetto

Oltre ai controlli della guida "Controllo finale", verifica in particolare:
1. Precisione: avvia un AMRAP di 20 minuti con un cronometro accanto: alla fine la differenza deve essere sotto il secondo.
Correggi quello che non va e scrivimi cosa hai cambiato.

Dettagli tecnici

Dalle impostazioni alle fasi del timer

Trasforma ogni tipo di allenamento in un elenco di fasi con inizio e fine: lo schermo mostra la fase in cui cade l'ora attuale.

type Fase = { tipo: "lavoro" | "riposo"; round: number; inizio: number; fine: number };  // secondi

export type Impostazioni =
  | { tipo: "amrap"; minuti: number }
  | { tipo: "emom"; minuti: number }
  | { tipo: "tabata"; lavoro: number; riposo: number; round: number }
  | { tipo: "fortime"; massimoMinuti: number };

export function creaFasi(i: Impostazioni): Fase[] {
  const fasi: Fase[] = [];
  let t = 0;
  const aggiungi = (tipo: Fase["tipo"], round: number, durata: number) => {
    fasi.push({ tipo, round, inizio: t, fine: t + durata });
    t += durata;
  };
  if (i.tipo === "amrap") aggiungi("lavoro", 1, i.minuti * 60);
  if (i.tipo === "fortime") aggiungi("lavoro", 1, i.massimoMinuti * 60);
  if (i.tipo === "emom") for (let r = 1; r <= i.minuti; r++) aggiungi("lavoro", r, 60);
  if (i.tipo === "tabata") for (let r = 1; r <= i.round; r++) {
    aggiungi("lavoro", r, i.lavoro);
    if (r < i.round) aggiungi("riposo", r, i.riposo);
  }
  return fasi;
}

// Fase attuale e secondi rimasti, dall'orologio (preciso anche se il telefono rallenta)
export function statoAttuale(fasi: Fase[], partenza: number) {
  const trascorsi = (performance.now() - partenza) / 1000;
  const fase = fasi.find((f) => trascorsi < f.fine);
  return fase ? { fase, rimasti: Math.ceil(fase.fine - trascorsi) } : null;   // null = finito
}

// Bip con il Web Audio API: acuto per il via, grave per il conto alla rovescia
export function bip(audio: AudioContext, acuto = false) {
  const osc = audio.createOscillator();
  const vol = audio.createGain();
  osc.frequency.value = acuto ? 1320 : 660;
  vol.gain.setValueAtTime(0.4, audio.currentTime);
  vol.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + (acuto ? 0.6 : 0.2));
  osc.connect(vol).connect(audio.destination);
  osc.start();
  osc.stop(audio.currentTime + 0.6);
}

Allenamenti e risultati

Gli allenamenti salvati e i risultati, per il grafico dei miglioramenti.

create table allenamenti (
  id uuid primary key default gen_random_uuid(),
  utente_id uuid not null references auth.users(id) on delete cascade,
  nome text not null,
  impostazioni jsonb not null,
  preferito boolean not null default false
);

create table risultati (
  id uuid primary key default gen_random_uuid(),
  allenamento_id uuid not null references allenamenti(id) on delete cascade,
  utente_id uuid not null references auth.users(id) on delete cascade,
  round_fatti int,
  tempo_secondi int,
  note text,
  fatto_il timestamptz not null default now()
);

alter table allenamenti enable row level security;
create policy "le mie righe" on allenamenti
  for all using (auth.uid() = utente_id) with check (auth.uid() = utente_id);

alter table risultati enable row level security;
create policy "le mie righe" on risultati
  for all using (auth.uid() = utente_id) with check (auth.uid() = utente_id);

Pezzi comuni a tutti i progetti

Glossario

JSONB
Un tipo di colonna che contiene dati strutturati flessibili, come un piccolo documento.
AMRAP / EMOM / Tabata
Formati di allenamento: più round possibili in un tempo dato / un esercizio all'inizio di ogni minuto / 20 secondi di lavoro e 10 di riposo.
Web Audio API
La funzione del browser che crea suoni direttamente dal codice, senza file audio.