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.