diff --git a/TUTORIAL_SYSTEM.md b/TUTORIAL_SYSTEM.md new file mode 100644 index 0000000..d897ac1 --- /dev/null +++ b/TUTORIAL_SYSTEM.md @@ -0,0 +1,217 @@ +# Tutorial System - MyTaskly + +Sistema di tutorial interattivo per guidare gli utenti attraverso le funzionalità principali dell'app MyTaskly. + +## Architettura + +Il sistema di tutorial è basato su `react-native-walkthrough-tooltip` e utilizza un approccio context-based per la gestione dello stato globale. + +### Componenti Principali + +``` +src/components/Tutorial/ +├── InteractiveTutorial.tsx # Componente base per tooltip interattivi +├── TutorialTooltip.tsx # Wrapper semplificato per applicare tooltip +├── TutorialWelcomeScreen.tsx # Schermata di benvenuto iniziale +├── index.tsx # TutorialManager (legacy - deprecato) +└── exports.ts # Esportazioni centralizzate +``` + +### Context & Hooks + +``` +src/contexts/TutorialContext.tsx # Context provider per stato tutorial +src/hooks/useInteractiveTutorial.ts # Hook per usare il tutorial context +``` + +### Configurazione + +``` +src/constants/tutorialContent.ts # Definizione degli step del tutorial +src/locales/ # Traduzioni IT/EN +``` + +## Come Funziona + +### 1. Avvio Automatico + +Al primo avvio dell'app, viene mostrata automaticamente la `TutorialWelcomeScreen` che offre due opzioni: +- **Inizia il Tour**: Avvia il tutorial interattivo +- **Salta il tour**: Chiude la schermata e salva la preferenza + +### 2. Step del Tutorial + +Gli step sono definiti in `tutorialContent.ts` e organizzati per schermata: + +**Home Screen:** +- `home-text-chat`: Input messaggio chat testuale +- `home-voice-chat`: Pulsante microfono per chat vocale +- `home-chat-history`: Pulsante cronologia conversazioni + +**Categories Screen:** +- `categories-general`: Vista categoria generale +- `categories-new-task`: Pulsante creazione task da categoria +- `categories-refresh`: Pulsante aggiorna/sincronizza + +**Notes Screen** (da implementare): +- `notes-create`: Creazione nuova nota +- `notes-edit`: Modifica nota esistente +- `notes-move`: Spostamento nota su whiteboard + +**Calendar Screen** (da implementare): +- `calendar-switch`: Switch tra vista base e avanzata + +### 3. Tooltip Interattivi + +Ogni elemento UI chiave è wrappato con ``: + +```tsx + + + {/* ... elemento UI ... */} + + +``` + +Il tooltip mostra: +- Titolo e descrizione dello step +- Pulsanti navigazione (Avanti/Indietro) +- Opzione "Salta tutorial" +- Posizionamento automatico (top/bottom/left/right) + +### 4. Persistenza + +Lo stato di completamento del tutorial viene salvato in AsyncStorage: +- Key: `@mytaskly:tutorial_completed` +- Values: `"true"` (completato), `"skipped"` (saltato), `null` (non fatto) + +## Utilizzo + +### Integrare un Nuovo Step + +1. **Aggiungi traduzione** in `src/locales/it.json` e `en.json`: +```json +"tutorial": { + "steps": { + "myScreen": { + "myFeature": { + "title": "Titolo Feature", + "description": "Descrizione dettagliata" + } + } + } +} +``` + +2. **Definisci lo step** in `src/constants/tutorialContent.ts`: +```typescript +{ + key: 'myScreen-myFeature', + screen: 'MyScreen', + title: content.myScreen.myFeature.title, + description: content.myScreen.myFeature.description, + placement: 'bottom', +} +``` + +3. **Wrappa l'elemento UI** nella schermata: +```tsx +import { TutorialTooltip } from '../../components/Tutorial/TutorialTooltip'; + + + + {/* ... */} + + +``` + +### Riavviare il Tutorial + +Gli utenti possono riavviare il tutorial da **Impostazioni > Rivedi Tutorial**. Questo: +- Rimuove il flag di completamento +- Imposta gli step del tutorial +- Avvia il tutorial dall'inizio + +## API Context + +### TutorialContext + +```typescript +interface TutorialContextType { + isTutorialVisible: boolean; + shouldAutoStart: boolean; + currentStepIndex: number; + currentStep: TutorialStep | null; + steps: TutorialStep[]; + startTutorial: () => void; + closeTutorial: () => void; + nextStep: () => void; + previousStep: () => void; + skipTutorial: () => void; + setSteps: (steps: TutorialStep[]) => void; + registerElementRef: (key: string, ref: any) => void; + getElementRef: (key: string) => any; +} +``` + +### Uso del Context + +```tsx +import { useTutorialContext } from '../contexts/TutorialContext'; + +const { + startTutorial, + nextStep, + currentStep +} = useTutorialContext(); +``` + +## Personalizzazione Stili + +Gli stili dei tooltip sono definiti in `InteractiveTutorial.tsx`: + +```typescript +const styles = StyleSheet.create({ + tooltipContent: { padding: 20, maxWidth: 300 }, + tooltipTitle: { fontSize: 18, fontWeight: 'bold' }, + tooltipDescription: { fontSize: 14, color: '#666' }, + primaryButton: { backgroundColor: '#007AFF' }, + // ... +}); +``` + +## Migrazione dal Vecchio Sistema + +Il vecchio sistema basato su `TutorialManager` è stato deprecato. Le differenze principali: + +| Vecchio | Nuovo | +|---------|-------| +| `TutorialManager` component | `TutorialWelcomeScreen` + `TutorialTooltip` | +| Navigation-based | Context-based | +| Spotlight overlay | Native tooltips | +| Sequenza fissa | Step modulari | +| Pulsante "?" nella Home | Avvio automatico + Settings | + +## Troubleshooting + +### Il tutorial non si avvia +- Verifica che `TutorialWelcomeScreen` sia montato in `navigation/index.tsx` +- Controlla AsyncStorage per il flag `@mytaskly:tutorial_completed` +- Verifica che gli step siano configurati correttamente + +### I tooltip non appaiono +- Verifica che `stepKey` corrisponda a uno step definito +- Controlla che `isTutorialVisible` sia `true` nel context +- Verifica il `currentStep.screen` corrisponda alla schermata corrente + +### Errori TypeScript +- Assicurati che `TutorialStep` sia importato da `InteractiveTutorial.tsx` +- Non confondere `TutorialStep` (nuovo) con `OldTutorialStep` (legacy) + +## Future Enhancements + +- [ ] Completare integrazione Notes e Calendar +- [ ] Aggiungere animazioni personalizzate +- [ ] Tracking analytics completamento tutorial +- [ ] Supporto per tooltip condizionali (es. solo se feature attiva) +- [ ] Tutorial contestuali (es. dopo aver completato un'azione) diff --git a/assets/tutorial/Chat_history.png b/assets/tutorial/Chat_history.png new file mode 100644 index 0000000..2c1a94b Binary files /dev/null and b/assets/tutorial/Chat_history.png differ diff --git a/assets/tutorial/Edit_category.png b/assets/tutorial/Edit_category.png new file mode 100644 index 0000000..c7fcc04 Binary files /dev/null and b/assets/tutorial/Edit_category.png differ diff --git a/assets/tutorial/Edit_task.png b/assets/tutorial/Edit_task.png new file mode 100644 index 0000000..574850a Binary files /dev/null and b/assets/tutorial/Edit_task.png differ diff --git a/assets/tutorial/Switch_calendar.png b/assets/tutorial/Switch_calendar.png new file mode 100644 index 0000000..76b1186 Binary files /dev/null and b/assets/tutorial/Switch_calendar.png differ diff --git a/assets/tutorial/Text_chat.png b/assets/tutorial/Text_chat.png new file mode 100644 index 0000000..6dc9604 Binary files /dev/null and b/assets/tutorial/Text_chat.png differ diff --git a/assets/tutorial/Voice_chat.png b/assets/tutorial/Voice_chat.png new file mode 100644 index 0000000..a207543 Binary files /dev/null and b/assets/tutorial/Voice_chat.png differ diff --git a/package-lock.json b/package-lock.json index ed12701..8ea95b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "react-native-blob-util": "^0.22.2", "react-native-calendars": "^1.1313.0", "react-native-chat-ui": "^0.1.9", + "react-native-copilot": "^3.3.3", "react-native-dotenv": "^3.4.11", "react-native-edge-to-edge": "1.6.0", "react-native-gesture-handler": "~2.24.0", @@ -73,6 +74,7 @@ "react-native-svg": "15.11.2", "react-native-track-player": "^4.1.2", "react-native-vector-icons": "^10.2.0", + "react-native-walkthrough-tooltip": "^1.6.0", "react-native-web": "^0.20.0", "react-native-webview": "13.13.5" }, @@ -1573,6 +1575,409 @@ "dev": true, "license": "MIT" }, + "node_modules/@changesets/apply-release-plan": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz", + "integrity": "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==", + "license": "MIT", + "dependencies": { + "@changesets/config": "^3.1.2", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz", + "integrity": "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==", + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/assemble-release-plan/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/changelog-github": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.2.tgz", + "integrity": "sha512-HeGeDl8HaIGj9fQHo/tv5XKQ2SNEi9+9yl1Bss1jttPqeiASRXhfi0A2wv8yFKCp07kR1gpOI5ge6+CWNm1jPw==", + "license": "MIT", + "dependencies": { + "@changesets/get-github-info": "^0.7.0", + "@changesets/types": "^6.1.0", + "dotenv": "^8.1.0" + } + }, + "node_modules/@changesets/changelog-github/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/cli": { + "version": "2.29.8", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.8.tgz", + "integrity": "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==", + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^7.0.14", + "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.2", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-release-plan": "^4.0.14", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.6", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "ci-info": "^3.7.0", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "p-limit": "^2.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" + }, + "bin": { + "changeset": "bin.js" + } + }, + "node_modules/@changesets/cli/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@changesets/cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/config": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.2.tgz", + "integrity": "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==", + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/logger": "^0.1.1", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/config/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "license": "MIT", + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz", + "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==", + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-dependents-graph/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/get-github-info": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.7.0.tgz", + "integrity": "sha512-+i67Bmhfj9V4KfDeS1+Tz3iF32btKZB2AAx+cYMqDSRFP7r3/ZdGbjCo+c6qkyViN9ygDuBjzageuPGJtKGe5A==", + "license": "MIT", + "dependencies": { + "dataloader": "^1.4.0", + "node-fetch": "^2.5.0" + } + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.14.tgz", + "integrity": "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==", + "license": "MIT", + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/config": "^3.1.2", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.6", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", + "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz", + "integrity": "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==", + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/pre/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.6.tgz", + "integrity": "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==", + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.2", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/read/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "license": "MIT" + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@changesets/write/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -2916,6 +3321,27 @@ "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", "license": "MIT" }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3519,6 +3945,96 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", @@ -3545,7 +4061,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "devOptional": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3559,7 +4074,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3569,7 +4083,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "devOptional": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -5816,6 +6329,15 @@ "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "license": "MIT" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5980,6 +6502,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -6462,6 +6993,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", + "license": "MIT", + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -6776,6 +7319,12 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -7393,6 +7942,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==", + "license": "BSD-3-Clause" + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -7565,6 +8120,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -7597,6 +8161,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7805,6 +8381,31 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -9159,6 +9760,12 @@ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9169,7 +9776,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "devOptional": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -9186,7 +9792,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9255,7 +9860,6 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "devOptional": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -9573,7 +10177,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "devOptional": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -9874,6 +10477,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10135,6 +10758,15 @@ "node": ">= 6" } }, + "node_modules/human-id": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -10182,6 +10814,22 @@ } } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -10544,7 +11192,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10609,7 +11256,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -10789,6 +11435,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", + "license": "MIT", + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/is-symbol": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", @@ -10881,6 +11539,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -12318,7 +12985,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "devOptional": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -12708,6 +13374,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "license": "MIT" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -13096,7 +13768,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 8" @@ -13669,6 +14340,12 @@ "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -13772,6 +14449,15 @@ "node": ">=18" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -14285,6 +14971,12 @@ "node": ">=8" } }, + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -14303,6 +14995,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "license": "MIT", + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14333,6 +15037,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -14348,6 +15061,15 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -14469,6 +15191,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14487,6 +15218,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -14641,6 +15381,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14823,6 +15578,22 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -14861,7 +15632,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "devOptional": true, "funding": [ { "type": "github", @@ -15142,6 +15912,22 @@ "react-native": "*" } }, + "node_modules/react-native-copilot": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/react-native-copilot/-/react-native-copilot-3.3.3.tgz", + "integrity": "sha512-/LX70DSqVhjOsbSGC3r3RYg+FtovUUudqWKby2agbXb0PsIWaEvjoH5SNQRkBCW8DHbDlZoPMv6oGKSJm3BPrQ==", + "license": "MIT", + "dependencies": { + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.27.1", + "mitt": "^3.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-native": ">=0.60.0", + "react-native-svg": ">=9.0.0" + } + }, "node_modules/react-native-dotenv": { "version": "3.4.11", "resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz", @@ -15472,6 +16258,25 @@ "node": ">=10" } }, + "node_modules/react-native-walkthrough-tooltip": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-native-walkthrough-tooltip/-/react-native-walkthrough-tooltip-1.6.0.tgz", + "integrity": "sha512-8DHpcqsF+2VULYnKJCSZtllhlFUFiZcWCwOlw5RLkS451ElxPWJn9ShOFVZtkPMcwQAirq3SovumVM7Y/li0Fw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.1", + "react-fast-compare": "^2.0.4" + }, + "peerDependencies": { + "@types/react": ">=16.8.24" + } + }, + "node_modules/react-native-walkthrough-tooltip/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" + }, "node_modules/react-native-web": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz", @@ -15683,6 +16488,52 @@ "react": "^19.0.0" } }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/read-yaml-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -15955,7 +16806,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "devOptional": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -16009,7 +16859,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "devOptional": true, "funding": [ { "type": "github", @@ -16107,7 +16956,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sax": { @@ -16734,6 +17582,28 @@ "memory-pager": "^1.0.2" } }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, + "node_modules/spawndamnit/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -17312,6 +18182,18 @@ "node": ">=8" } }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -18014,7 +18896,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 4.0.0" diff --git a/package.json b/package.json index 09ef123..c9224bb 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "react-native-blob-util": "^0.22.2", "react-native-calendars": "^1.1313.0", "react-native-chat-ui": "^0.1.9", + "react-native-copilot": "^3.3.3", "react-native-dotenv": "^3.4.11", "react-native-edge-to-edge": "1.6.0", "react-native-gesture-handler": "~2.24.0", @@ -83,6 +84,7 @@ "react-native-svg": "15.11.2", "react-native-track-player": "^4.1.2", "react-native-vector-icons": "^10.2.0", + "react-native-walkthrough-tooltip": "^1.6.0", "react-native-web": "^0.20.0", "react-native-webview": "13.13.5" }, diff --git a/src/components/Category/CategoryView.tsx b/src/components/Category/CategoryView.tsx index 37ab3e6..8bcdfdc 100644 --- a/src/components/Category/CategoryView.tsx +++ b/src/components/Category/CategoryView.tsx @@ -149,21 +149,25 @@ const CategoryView = forwardRef( {/* Mostra le categorie solo se non stiamo ricaricando */} {!isReloading && categories && categories.length > 0 - ? categories.map((category, index) => ( - - )) + ? categories.map((category, index) => { + const categoryElement = ( + + ); + + return categoryElement; + }) : !loading && !isReloading && ( diff --git a/src/components/Tutorial/CompletionScreen.tsx b/src/components/Tutorial/CompletionScreen.tsx deleted file mode 100644 index b03f5f8..0000000 --- a/src/components/Tutorial/CompletionScreen.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import { View, Text, Pressable, Animated } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { tutorialStyles } from './styles'; -import { TUTORIAL_CONTENT } from '../../constants/tutorialContent'; - -export interface CompletionScreenProps { - onComplete: () => void; - onReview: () => void; -} - -export const CompletionScreen: React.FC = ({ - onComplete, - onReview, -}) => { - const fadeAnim = useRef(new Animated.Value(0)).current; - const scaleAnim = useRef(new Animated.Value(0.9)).current; - const iconScaleAnim = useRef(new Animated.Value(0)).current; - - useEffect(() => { - Animated.sequence([ - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }), - Animated.spring(scaleAnim, { - toValue: 1, - tension: 50, - friction: 7, - useNativeDriver: true, - }), - ]), - Animated.spring(iconScaleAnim, { - toValue: 1, - tension: 50, - friction: 5, - useNativeDriver: true, - }), - ]).start(); - }, [fadeAnim, scaleAnim, iconScaleAnim]); - - return ( - - - {/* Success Icon */} - - - - - {/* Title */} - - {TUTORIAL_CONTENT.completion.title} - - - {/* Description */} - - {TUTORIAL_CONTENT.completion.description} - - - {/* Buttons */} - - [ - tutorialStyles.welcomeButton, - tutorialStyles.welcomePrimaryButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onComplete} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.completion.primaryButton} - accessibilityHint="Completa il tutorial e inizia a usare l'app" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.completion.primaryButton} - - )} - - - [ - tutorialStyles.welcomeButton, - tutorialStyles.welcomeSecondaryButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onReview} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.completion.secondaryButton} - accessibilityHint="Rivedi il tutorial dall'inizio" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.completion.secondaryButton} - - )} - - - - - ); -}; diff --git a/src/components/Tutorial/NavigationControls.tsx b/src/components/Tutorial/NavigationControls.tsx deleted file mode 100644 index 726faab..0000000 --- a/src/components/Tutorial/NavigationControls.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React from 'react'; -import { View, Text, Pressable } from 'react-native'; -import { tutorialStyles } from './styles'; -import { TUTORIAL_CONTENT } from '../../constants/tutorialContent'; - -export interface NavigationControlsProps { - canGoBack: boolean; - canGoNext: boolean; - onBack: () => void; - onNext: () => void; - onSkip: () => void; - showSkip?: boolean; -} - -export const NavigationControls: React.FC = ({ - canGoBack, - canGoNext, - onBack, - onNext, - onSkip, - showSkip = true, -}) => { - return ( - - {/* Back Button */} - {canGoBack ? ( - [ - tutorialStyles.navigationButton, - tutorialStyles.secondaryButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onBack} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.navigation.back} - accessibilityHint="Torna al passo precedente" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.navigation.back} - - )} - - ) : ( - showSkip && ( - [ - tutorialStyles.navigationButton, - tutorialStyles.skipButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onSkip} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.navigation.skip} - accessibilityHint="Salta il tutorial" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.navigation.skip} - - )} - - ) - )} - - {/* Next Button */} - {canGoNext && ( - [ - tutorialStyles.navigationButton, - tutorialStyles.primaryButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onNext} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.navigation.next} - accessibilityHint="Vai al passo successivo" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.navigation.next} - - )} - - )} - - ); -}; diff --git a/src/components/Tutorial/ProgressIndicator.tsx b/src/components/Tutorial/ProgressIndicator.tsx deleted file mode 100644 index 9ce7f2d..0000000 --- a/src/components/Tutorial/ProgressIndicator.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { View } from 'react-native'; -import { tutorialStyles } from './styles'; - -export interface ProgressIndicatorProps { - totalSteps: number; - currentStep: number; -} - -export const ProgressIndicator: React.FC = ({ - totalSteps, - currentStep -}) => { - return ( - - {Array.from({ length: totalSteps }).map((_, index) => ( - - ))} - - ); -}; diff --git a/src/components/Tutorial/TooltipCard.tsx b/src/components/Tutorial/TooltipCard.tsx deleted file mode 100644 index 2787372..0000000 --- a/src/components/Tutorial/TooltipCard.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import { View, Text, Animated, Dimensions } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { tutorialStyles } from './styles'; -import { ElementMeasurement } from '../../hooks/useTutorial'; - -export interface TooltipCardProps { - title: string; - description: string; - icon?: string; - targetMeasurement?: ElementMeasurement | null; -} - -const { height: SCREEN_HEIGHT } = Dimensions.get('window'); - -export const TooltipCard: React.FC = ({ - title, - description, - icon, - targetMeasurement, -}) => { - const slideAnim = useRef(new Animated.Value(50)).current; - const fadeAnim = useRef(new Animated.Value(0)).current; - - useEffect(() => { - // Reset animations when target changes - slideAnim.setValue(50); - fadeAnim.setValue(0); - - // Animate in - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }), - Animated.timing(slideAnim, { - toValue: 0, - duration: 400, - useNativeDriver: true, - }), - ]).start(); - }, [fadeAnim, slideAnim, targetMeasurement]); - - // Calculate position based on spotlight location - const getTooltipPosition = () => { - if (!targetMeasurement) { - // Default center position if no target - return { - top: SCREEN_HEIGHT / 2 - 150, - left: 32, - right: 32, - }; - } - - const { pageY, height } = targetMeasurement; - const spotlightBottom = pageY + height; - const isInTopThird = pageY < SCREEN_HEIGHT / 3; - const isInBottomThird = spotlightBottom > (SCREEN_HEIGHT * 2) / 3; - - if (isInBottomThird) { - // Position above the spotlight - return { - bottom: SCREEN_HEIGHT - pageY + 24, - left: 32, - right: 32, - }; - } else if (isInTopThird) { - // Position below the spotlight - return { - top: spotlightBottom + 24, - left: 32, - right: 32, - }; - } else { - // Position below the spotlight (default) - return { - top: spotlightBottom + 24, - left: 32, - right: 32, - }; - } - }; - - const position = getTooltipPosition(); - - return ( - - {/* Icon */} - {icon && ( - - - - )} - - {/* Title */} - - {title} - - - {/* Description */} - - {description} - - - ); -}; diff --git a/src/components/Tutorial/TutorialOverlay.tsx b/src/components/Tutorial/TutorialOverlay.tsx deleted file mode 100644 index d0b2c30..0000000 --- a/src/components/Tutorial/TutorialOverlay.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import { View, Animated, Dimensions, Easing } from 'react-native'; -import { tutorialStyles } from './styles'; -import { TooltipCard } from './TooltipCard'; -import { NavigationControls } from './NavigationControls'; -import { ProgressIndicator } from './ProgressIndicator'; -import { ElementMeasurement } from '../../hooks/useTutorial'; - -export interface TutorialOverlayProps { - targetMeasurement: ElementMeasurement | null; - currentStepIndex: number; - totalSteps: number; - title: string; - description: string; - icon?: string; - canGoBack: boolean; - canGoNext: boolean; - onBack: () => void; - onNext: () => void; - onSkip: () => void; -} - -const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); - -export const TutorialOverlay: React.FC = ({ - targetMeasurement, - currentStepIndex, - totalSteps, - title, - description, - icon, - canGoBack, - canGoNext, - onBack, - onNext, - onSkip, -}) => { - const overlayOpacity = useRef(new Animated.Value(0)).current; - const pulseAnim = useRef(new Animated.Value(1)).current; - - useEffect(() => { - // Fade in overlay - Animated.timing(overlayOpacity, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }).start(); - }, [overlayOpacity]); - - useEffect(() => { - // Pulse animation for spotlight - if (targetMeasurement) { - const pulse = Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1.05, - duration: 1500, - easing: Easing.inOut(Easing.ease), - useNativeDriver: true, - }), - Animated.timing(pulseAnim, { - toValue: 1, - duration: 1500, - easing: Easing.inOut(Easing.ease), - useNativeDriver: true, - }), - ]) - ); - pulse.start(); - - return () => { - pulse.stop(); - }; - } - }, [pulseAnim, targetMeasurement]); - - // Calculate spotlight style - const getSpotlightStyle = () => { - if (!targetMeasurement) { - return { - display: 'none' as const, - }; - } - - const { pageX, pageY, width, height } = targetMeasurement; - - // Add padding around the element - const padding = 8; - const borderRadius = Math.min(width, height) > 100 ? 16 : 24; - - return { - left: pageX - padding, - top: pageY - padding, - width: width + padding * 2, - height: height + padding * 2, - borderRadius, - }; - }; - - const spotlightStyle = getSpotlightStyle(); - - return ( - - {/* Spotlight */} - {targetMeasurement && spotlightStyle.display !== 'none' && ( - - )} - - {/* Tooltip Card */} - - - {/* Progress Indicator */} - - - - - {/* Navigation Controls */} - - - - - ); -}; diff --git a/src/components/Tutorial/WelcomeScreen.tsx b/src/components/Tutorial/WelcomeScreen.tsx deleted file mode 100644 index 6e87638..0000000 --- a/src/components/Tutorial/WelcomeScreen.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import { View, Text, Pressable, Animated, Image } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { tutorialStyles } from './styles'; -import { TUTORIAL_CONTENT } from '../../constants/tutorialContent'; - -export interface WelcomeScreenProps { - onStart: () => void; - onSkip: () => void; -} - -export const WelcomeScreen: React.FC = ({ onStart, onSkip }) => { - const fadeAnim = useRef(new Animated.Value(0)).current; - const scaleAnim = useRef(new Animated.Value(0.9)).current; - - useEffect(() => { - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }), - Animated.spring(scaleAnim, { - toValue: 1, - tension: 50, - friction: 7, - useNativeDriver: true, - }), - ]).start(); - }, [fadeAnim, scaleAnim]); - - return ( - - - {/* Logo/Icon */} - - - - - {/* Title */} - - {TUTORIAL_CONTENT.welcome.title} - - - {/* Description */} - - {TUTORIAL_CONTENT.welcome.description} - - - {/* Buttons */} - - [ - tutorialStyles.welcomeButton, - tutorialStyles.welcomePrimaryButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onStart} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.welcome.startButton} - accessibilityHint="Inizia il tour guidato dell'app" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.welcome.startButton} - - )} - - - [ - tutorialStyles.welcomeButton, - tutorialStyles.welcomeSecondaryButton, - pressed && { transform: [{ scale: 0.95 }] }, - ]} - onPress={onSkip} - accessibilityRole="button" - accessibilityLabel={TUTORIAL_CONTENT.welcome.skipButton} - accessibilityHint="Salta il tour e vai direttamente all'app" - > - {({ pressed }) => ( - - {TUTORIAL_CONTENT.welcome.skipButton} - - )} - - - - - ); -}; diff --git a/src/components/Tutorial/exports.ts b/src/components/Tutorial/exports.ts new file mode 100644 index 0000000..80da534 --- /dev/null +++ b/src/components/Tutorial/exports.ts @@ -0,0 +1 @@ +export { TutorialOnboarding } from './index'; diff --git a/src/components/Tutorial/index.tsx b/src/components/Tutorial/index.tsx index 67896a6..00de037 100644 --- a/src/components/Tutorial/index.tsx +++ b/src/components/Tutorial/index.tsx @@ -1,126 +1,692 @@ -import React from 'react'; -import { Modal } from 'react-native'; -import { NavigationProp } from '@react-navigation/native'; -import { useTutorial } from '../../hooks/useTutorial'; -import { useTutorialContext } from '../../contexts/TutorialContext'; -import { WelcomeScreen } from './WelcomeScreen'; -import { CompletionScreen } from './CompletionScreen'; -import { TutorialOverlay } from './TutorialOverlay'; -import { RootStackParamList, TabParamList } from '../../navigation'; - -export interface TutorialManagerProps { - navigation: NavigationProp | NavigationProp; - onComplete?: () => void; - autoStart?: boolean; +import React, { useRef, useState, useCallback } from 'react'; +import { + View, + Text, + Image, + Modal, + StyleSheet, + Dimensions, + FlatList, + Pressable, + Animated, + NativeSyntheticEvent, + NativeScrollEvent, + Platform, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useTranslation } from 'react-i18next'; +import { + getTutorialSections, + TUTORIAL_STORAGE_KEY, + TutorialStep, +} from '../../constants/tutorialContent'; + +const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); + +// Total pages = welcome + steps + completion +type PageType = 'welcome' | 'step' | 'section-header' | 'completion'; + +interface PageData { + type: PageType; + key: string; + step?: TutorialStep; + sectionTitle?: string; + sectionKey?: string; } -export const TutorialManager: React.FC = ({ - navigation, - onComplete, - autoStart = false, -}) => { - // Get tutorial visibility from context - const tutorialContext = useTutorialContext(); - const { isTutorialVisible, closeTutorial: closeContextTutorial } = tutorialContext; - - console.log('[TUTORIAL_MANAGER] 📱 Component render - isTutorialVisible:', isTutorialVisible); - - const { - isVisible, - currentStep, - currentStepIndex, - totalSteps, - targetMeasurement, - isNavigating, - nextStep, - previousStep, - skipTutorial, - completeTutorial, - restartTutorial, - canGoBack, - canGoNext, - startTutorial, - } = useTutorial({ - navigation, - onComplete, - onSkip: onComplete, - }); - - // Start tutorial when context says it should be visible - React.useEffect(() => { - console.log('[TUTORIAL_MANAGER] isTutorialVisible from context:', isTutorialVisible); - console.log('[TUTORIAL_MANAGER] isVisible from hook:', isVisible); - - if (isTutorialVisible && !isVisible) { - console.log('[TUTORIAL_MANAGER] 🚀 Starting tutorial from context trigger (FORCE MODE)'); - startTutorial(true); // Force start when manually triggered +function buildPages(): PageData[] { + const sections = getTutorialSections(); + const pages: PageData[] = []; + + // Welcome page + pages.push({ type: 'welcome', key: 'welcome' }); + + // Steps grouped by section with section headers + for (const section of sections) { + // Section header page + pages.push({ + type: 'section-header', + key: `section-${section.key}`, + sectionTitle: section.title, + sectionKey: section.key, + }); + + // Step pages + for (const step of section.steps) { + pages.push({ + type: 'step', + key: step.key, + step, + sectionTitle: section.title, + }); } - }, [isTutorialVisible, isVisible, startTutorial]); + } - console.log('[TUTORIAL_MANAGER] Rendering - isVisible:', isVisible, 'currentStep:', currentStep?.type); + // Completion page + pages.push({ type: 'completion', key: 'completion' }); - if (!isVisible || !currentStep) { - console.log('[TUTORIAL_MANAGER] Not rendering - isVisible:', isVisible, 'currentStep:', !!currentStep); - return null; + return pages; +} + +// Section icon mapping +function getSectionIcon(sectionKey?: string): keyof typeof Ionicons.glyphMap { + switch (sectionKey) { + case 'home': return 'home'; + case 'categories': return 'grid'; + case 'calendar': return 'calendar'; + default: return 'apps'; } +} - const handleWelcomeStart = () => { - nextStep(); - }; +export const TutorialOnboarding: React.FC<{ + visible: boolean; + onComplete: () => void; + onSkip: () => void; +}> = ({ visible, onComplete, onSkip }) => { + const { t } = useTranslation(); + const flatListRef = useRef(null); + const scrollX = useRef(new Animated.Value(0)).current; + const [currentIndex, setCurrentIndex] = useState(0); - const handleCompletionReview = () => { - restartTutorial(); - }; + const pages = React.useMemo(() => buildPages(), []); + const totalPages = pages.length; + + const handleScroll = Animated.event( + [{ nativeEvent: { contentOffset: { x: scrollX } } }], + { useNativeDriver: false } + ); - const handleSkip = () => { - skipTutorial(); - closeContextTutorial(); // Reset context state + const onMomentumScrollEnd = useCallback( + (e: NativeSyntheticEvent) => { + const index = Math.round(e.nativeEvent.contentOffset.x / SCREEN_WIDTH); + setCurrentIndex(index); + }, + [] + ); + + const goToPage = useCallback( + (index: number) => { + flatListRef.current?.scrollToIndex({ index, animated: true }); + setCurrentIndex(index); + }, + [] + ); + + const handleNext = useCallback(() => { + if (currentIndex < totalPages - 1) { + goToPage(currentIndex + 1); + } + }, [currentIndex, totalPages, goToPage]); + + const handleBack = useCallback(() => { + if (currentIndex > 0) { + goToPage(currentIndex - 1); + } + }, [currentIndex, goToPage]); + + const handleComplete = useCallback(async () => { + try { + await AsyncStorage.setItem(TUTORIAL_STORAGE_KEY, 'true'); + } catch (error) { + console.error('[Tutorial] Error saving completion:', error); + } + setCurrentIndex(0); + onComplete(); + }, [onComplete]); + + const handleSkip = useCallback(async () => { + try { + await AsyncStorage.setItem(TUTORIAL_STORAGE_KEY, 'skipped'); + } catch (error) { + console.error('[Tutorial] Error saving skip:', error); + } + setCurrentIndex(0); + onSkip(); + }, [onSkip]); + + const handleReview = useCallback(() => { + goToPage(0); + }, [goToPage]); + + const renderWelcomePage = () => ( + + + + {t('tutorial.welcome.title')} + + {t('tutorial.welcome.description')} + + [ + styles.primaryButton, + pressed && styles.buttonPressed, + ]} + onPress={handleNext} + > + + {t('tutorial.welcome.startButton')} + + + [ + styles.secondaryButton, + pressed && styles.buttonPressed, + ]} + onPress={handleSkip} + > + + {t('tutorial.welcome.skipButton')} + + + + + ); + + const renderSectionHeader = (page: PageData) => ( + + + + + + {page.sectionTitle} + + {t('tutorial.navigation.next')} + + + + + ); + + const renderStepPage = (page: PageData) => { + if (!page.step) return null; + const { step } = page; + + return ( + + + {/* Section badge */} + + + {page.sectionTitle} + + + {/* Screenshot image */} + + + + + {/* Step title and description */} + {step.title} + {step.description} + + + ); }; - const handleComplete = () => { - completeTutorial(); - closeContextTutorial(); // Reset context state + const renderCompletionPage = () => ( + + + + + + + {t('tutorial.completion.title')} + + + {t('tutorial.completion.description')} + + [ + styles.primaryButton, + pressed && styles.buttonPressed, + ]} + onPress={handleComplete} + > + + {t('tutorial.completion.primaryButton')} + + + [ + styles.secondaryButton, + pressed && styles.buttonPressed, + ]} + onPress={handleReview} + > + + {t('tutorial.completion.secondaryButton')} + + + + + ); + + const renderPage = ({ item }: { item: PageData }) => { + switch (item.type) { + case 'welcome': + return renderWelcomePage(); + case 'section-header': + return renderSectionHeader(item); + case 'step': + return renderStepPage(item); + case 'completion': + return renderCompletionPage(); + default: + return null; + } }; + // Progress dots (exclude welcome and completion from count) + const stepPages = pages.filter(p => p.type === 'step' || p.type === 'section-header'); + const isWelcome = currentIndex === 0; + const isCompletion = currentIndex === totalPages - 1; + return ( - {currentStep.type === 'welcome' && ( - - )} - - {currentStep.type === 'spotlight' && !isNavigating && ( - - )} + + {/* Skip button (only on step pages) */} + {!isWelcome && !isCompletion && ( + [ + styles.skipHeaderButton, + pressed && { opacity: 0.6 }, + ]} + onPress={handleSkip} + > + + {t('tutorial.navigation.skip')} + + + )} - {currentStep.type === 'completion' && ( - item.key} + renderItem={renderPage} + horizontal + pagingEnabled + showsHorizontalScrollIndicator={false} + bounces={false} + onScroll={handleScroll} + onMomentumScrollEnd={onMomentumScrollEnd} + scrollEventThrottle={16} + getItemLayout={(_, index) => ({ + length: SCREEN_WIDTH, + offset: SCREEN_WIDTH * index, + index, + })} /> - )} + + {/* Bottom navigation bar (for step pages only) */} + {!isWelcome && !isCompletion && ( + + {/* Progress dots */} + + {stepPages.map((page, idx) => { + const pageIndex = pages.indexOf(page); + const isActive = pageIndex === currentIndex; + const isPast = pageIndex < currentIndex; + + return ( + + ); + })} + + + {/* Nav buttons */} + + [ + styles.navButton, + styles.navButtonBack, + pressed && { opacity: 0.6 }, + ]} + onPress={handleBack} + > + + + {t('tutorial.navigation.back')} + + + + [ + styles.navButton, + styles.navButtonNext, + pressed && { opacity: 0.8 }, + ]} + onPress={handleNext} + > + + {t('tutorial.navigation.next')} + + + + + + )} + ); }; -// Export hook for external use -export { useTutorial } from '../../hooks/useTutorial'; +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + + // Skip button top-right + skipHeaderButton: { + position: 'absolute', + top: Platform.OS === 'ios' ? 56 : 40, + right: 20, + zIndex: 10, + paddingVertical: 8, + paddingHorizontal: 16, + }, + skipHeaderText: { + fontSize: 16, + fontWeight: '500', + color: '#999', + }, + + // Page container (each page) + pageContainer: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 24, + }, + + // Welcome page + welcomeCard: { + alignItems: 'center', + width: '100%', + maxWidth: 360, + }, + welcomeLogo: { + width: 100, + height: 100, + marginBottom: 32, + }, + welcomeTitle: { + fontSize: 28, + fontWeight: '700', + color: '#000', + textAlign: 'center', + marginBottom: 16, + letterSpacing: -0.5, + }, + welcomeDescription: { + fontSize: 17, + fontWeight: '400', + color: '#666', + textAlign: 'center', + lineHeight: 26, + marginBottom: 40, + }, + + // Section header page + sectionHeaderCard: { + alignItems: 'center', + width: '100%', + maxWidth: 360, + }, + sectionIconContainer: { + width: 96, + height: 96, + borderRadius: 48, + backgroundColor: '#F5F5F5', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 24, + }, + sectionHeaderTitle: { + fontSize: 32, + fontWeight: '700', + color: '#000', + textAlign: 'center', + letterSpacing: -0.5, + }, + sectionHeaderSubtitle: { + fontSize: 16, + fontWeight: '400', + color: '#999', + textAlign: 'center', + marginTop: 12, + }, + + // Step page + stepContent: { + alignItems: 'center', + width: '100%', + maxWidth: 360, + paddingTop: Platform.OS === 'ios' ? 80 : 60, + }, + sectionBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F5F5F5', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + gap: 6, + marginBottom: 20, + }, + sectionBadgeText: { + fontSize: 13, + fontWeight: '600', + color: '#000', + }, + imageContainer: { + width: SCREEN_WIDTH - 64, + height: SCREEN_HEIGHT * 0.42, + backgroundColor: '#F8F8F8', + borderRadius: 16, + overflow: 'hidden', + marginBottom: 24, + justifyContent: 'center', + alignItems: 'center', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.1, + shadowRadius: 12, + }, + android: { + elevation: 4, + }, + }), + }, + stepImage: { + width: '100%', + height: '100%', + }, + stepTitle: { + fontSize: 22, + fontWeight: '700', + color: '#000', + textAlign: 'center', + marginBottom: 10, + letterSpacing: -0.3, + }, + stepDescription: { + fontSize: 15, + fontWeight: '400', + color: '#666', + textAlign: 'center', + lineHeight: 22, + paddingHorizontal: 8, + }, + + // Completion page + completionCard: { + alignItems: 'center', + width: '100%', + maxWidth: 360, + }, + completionIconContainer: { + width: 88, + height: 88, + borderRadius: 44, + backgroundColor: '#000', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 24, + }, + completionTitle: { + fontSize: 28, + fontWeight: '700', + color: '#000', + textAlign: 'center', + marginBottom: 16, + letterSpacing: -0.5, + }, + completionDescription: { + fontSize: 17, + fontWeight: '400', + color: '#666', + textAlign: 'center', + lineHeight: 26, + marginBottom: 40, + }, + + // Buttons + primaryButton: { + backgroundColor: '#000', + paddingVertical: 16, + borderRadius: 28, + width: '100%', + alignItems: 'center', + marginBottom: 12, + }, + secondaryButton: { + backgroundColor: 'transparent', + paddingVertical: 14, + borderRadius: 28, + width: '100%', + alignItems: 'center', + borderWidth: 1, + borderColor: '#E0E0E0', + }, + primaryButtonText: { + fontSize: 17, + fontWeight: '600', + color: '#FFF', + }, + secondaryButtonText: { + fontSize: 17, + fontWeight: '600', + color: '#000', + }, + buttonPressed: { + transform: [{ scale: 0.97 }], + opacity: 0.9, + }, + + // Bottom bar + bottomBar: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + paddingHorizontal: 24, + paddingBottom: Platform.OS === 'ios' ? 40 : 24, + paddingTop: 16, + backgroundColor: '#FFF', + }, + + // Progress dots + progressContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: 6, + marginBottom: 16, + }, + progressDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: '#E0E0E0', + }, + progressDotActive: { + width: 24, + borderRadius: 4, + backgroundColor: '#000', + }, + progressDotPast: { + backgroundColor: '#666', + }, + progressDotSection: { + width: 12, + height: 8, + borderRadius: 4, + }, + + // Nav buttons + navButtonsContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + navButton: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 24, + gap: 8, + }, + navButtonBack: { + backgroundColor: '#F5F5F5', + }, + navButtonNext: { + backgroundColor: '#000', + }, + navButtonBackText: { + fontSize: 16, + fontWeight: '600', + color: '#000', + }, + navButtonNextText: { + fontSize: 16, + fontWeight: '600', + color: '#FFF', + }, +}); diff --git a/src/components/Tutorial/styles.ts b/src/components/Tutorial/styles.ts deleted file mode 100644 index feef3c7..0000000 --- a/src/components/Tutorial/styles.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { StyleSheet, Platform } from 'react-native'; - -export const tutorialStyles = StyleSheet.create({ - // Overlay - overlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.75)', - zIndex: 9999, - }, - - // Spotlight - spotlightContainer: { - ...StyleSheet.absoluteFillObject, - }, - spotlight: { - position: 'absolute', - borderWidth: 3, - borderColor: '#FFFFFF', - ...Platform.select({ - ios: { - shadowColor: '#FFFFFF', - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0.6, - shadowRadius: 20, - }, - android: { - elevation: 8, - }, - }), - }, - - // Tooltip Card - tooltipCard: { - position: 'absolute', - backgroundColor: '#FFFFFF', - borderRadius: 16, - padding: 24, - maxWidth: 320, - ...Platform.select({ - ios: { - shadowColor: '#000000', - shadowOffset: { width: 0, height: 8 }, - shadowOpacity: 0.25, - shadowRadius: 16, - }, - android: { - elevation: 8, - }, - }), - }, - tooltipIconContainer: { - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: '#F5F5F5', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 16, - }, - tooltipTitle: { - fontSize: 20, - fontWeight: '600', - color: '#000000', - marginBottom: 8, - fontFamily: 'System', - letterSpacing: -0.5, - }, - tooltipDescription: { - fontSize: 16, - fontWeight: '400', - color: '#666666', - lineHeight: 24, - fontFamily: 'System', - }, - - // Progress Indicator - progressContainer: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - gap: 8, - marginTop: 24, - }, - progressDot: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: '#CCCCCC', - }, - progressDotActive: { - width: 12, - height: 12, - borderRadius: 6, - backgroundColor: '#000000', - }, - - // Navigation Controls - navigationContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginTop: 24, - }, - navigationButton: { - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 24, - minHeight: 44, - minWidth: 44, - justifyContent: 'center', - alignItems: 'center', - }, - primaryButton: { - backgroundColor: '#000000', - }, - secondaryButton: { - backgroundColor: '#F5F5F5', - }, - skipButton: { - backgroundColor: 'transparent', - }, - buttonText: { - fontSize: 16, - fontWeight: '600', - fontFamily: 'System', - }, - primaryButtonText: { - color: '#FFFFFF', - }, - secondaryButtonText: { - color: '#000000', - }, - skipButtonText: { - color: '#FFFFFF', - }, - - // Welcome Screen - welcomeContainer: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.95)', - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 32, - }, - welcomeCard: { - backgroundColor: '#FFFFFF', - borderRadius: 24, - padding: 40, - alignItems: 'center', - maxWidth: 400, - width: '100%', - ...Platform.select({ - ios: { - shadowColor: '#000000', - shadowOffset: { width: 0, height: 12 }, - shadowOpacity: 0.3, - shadowRadius: 24, - }, - android: { - elevation: 12, - }, - }), - }, - welcomeLogo: { - width: 80, - height: 80, - marginBottom: 24, - }, - welcomeTitle: { - fontSize: 28, - fontWeight: '700', - color: '#000000', - textAlign: 'center', - marginBottom: 16, - fontFamily: 'System', - letterSpacing: -0.8, - }, - welcomeDescription: { - fontSize: 17, - fontWeight: '400', - color: '#666666', - textAlign: 'center', - lineHeight: 26, - marginBottom: 32, - fontFamily: 'System', - }, - welcomeButtonContainer: { - width: '100%', - gap: 12, - }, - welcomeButton: { - paddingVertical: 16, - borderRadius: 28, - justifyContent: 'center', - alignItems: 'center', - minHeight: 56, - }, - welcomePrimaryButton: { - backgroundColor: '#000000', - }, - welcomeSecondaryButton: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: '#E0E0E0', - }, - welcomeButtonText: { - fontSize: 17, - fontWeight: '600', - fontFamily: 'System', - }, - - // Completion Screen - completionContainer: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.95)', - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 32, - }, - completionCard: { - backgroundColor: '#FFFFFF', - borderRadius: 24, - padding: 40, - alignItems: 'center', - maxWidth: 400, - width: '100%', - ...Platform.select({ - ios: { - shadowColor: '#000000', - shadowOffset: { width: 0, height: 12 }, - shadowOpacity: 0.3, - shadowRadius: 24, - }, - android: { - elevation: 12, - }, - }), - }, - completionIconContainer: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#4CAF50', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 24, - }, - completionTitle: { - fontSize: 28, - fontWeight: '700', - color: '#000000', - textAlign: 'center', - marginBottom: 16, - fontFamily: 'System', - letterSpacing: -0.8, - }, - completionDescription: { - fontSize: 17, - fontWeight: '400', - color: '#666666', - textAlign: 'center', - lineHeight: 26, - marginBottom: 32, - fontFamily: 'System', - }, - completionButtonContainer: { - width: '100%', - gap: 12, - }, - - // Loading State - loadingOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.3)', - justifyContent: 'center', - alignItems: 'center', - zIndex: 10000, - }, - loadingText: { - fontSize: 16, - color: '#FFFFFF', - marginTop: 16, - fontFamily: 'System', - }, -}); diff --git a/src/constants/tutorialContent.ts b/src/constants/tutorialContent.ts index 0c0777f..09540ea 100644 --- a/src/constants/tutorialContent.ts +++ b/src/constants/tutorialContent.ts @@ -1,4 +1,21 @@ import i18n from '../services/i18n'; +import { ImageSourcePropType } from 'react-native'; + +// Tutorial step with image support +export interface TutorialStep { + key: string; + section: 'home' | 'categories' | 'calendar'; + title: string; + description: string; + image: ImageSourcePropType; +} + +// Section header data +export interface TutorialSection { + key: string; + title: string; + steps: TutorialStep[]; +} // Tutorial content using i18n translations export const getTutorialContent = () => ({ @@ -8,30 +25,36 @@ export const getTutorialContent = () => ({ startButton: i18n.t('tutorial.welcome.startButton'), skipButton: i18n.t('tutorial.welcome.skipButton'), }, - step2: { - title: i18n.t('tutorial.steps.chat.title'), - description: i18n.t('tutorial.steps.chat.description'), - icon: "chatbubble-ellipses", - }, - step3: { - title: i18n.t('tutorial.steps.categories.title'), - description: i18n.t('tutorial.steps.categories.description'), - icon: "grid", - }, - step4: { - title: i18n.t('tutorial.steps.tasks.title'), - description: i18n.t('tutorial.steps.tasks.description'), - icon: "checkbox", + sections: { + home: i18n.t('tutorial.sections.home'), + categories: i18n.t('tutorial.sections.categories'), + calendar: i18n.t('tutorial.sections.calendar'), }, - step5: { - title: i18n.t('tutorial.steps.notes.title'), - description: i18n.t('tutorial.steps.notes.description'), - icon: "brush", - }, - step6: { - title: i18n.t('tutorial.steps.calendar.title'), - description: i18n.t('tutorial.steps.calendar.description'), - icon: "calendar", + steps: { + homeTextChat: { + title: i18n.t('tutorial.steps.home.textChat.title'), + description: i18n.t('tutorial.steps.home.textChat.description'), + }, + homeVoiceChat: { + title: i18n.t('tutorial.steps.home.voiceChat.title'), + description: i18n.t('tutorial.steps.home.voiceChat.description'), + }, + homeChatHistory: { + title: i18n.t('tutorial.steps.home.chatHistory.title'), + description: i18n.t('tutorial.steps.home.chatHistory.description'), + }, + categoriesEditCategory: { + title: i18n.t('tutorial.steps.categories.editCategory.title'), + description: i18n.t('tutorial.steps.categories.editCategory.description'), + }, + categoriesEditTask: { + title: i18n.t('tutorial.steps.categories.editTask.title'), + description: i18n.t('tutorial.steps.categories.editTask.description'), + }, + calendarSwitch: { + title: i18n.t('tutorial.steps.calendar.switch.title'), + description: i18n.t('tutorial.steps.calendar.switch.description'), + }, }, completion: { title: i18n.t('tutorial.completion.title'), @@ -47,105 +70,99 @@ export const getTutorialContent = () => ({ }); // For backward compatibility, export as constant that updates with language -export const TUTORIAL_CONTENT = getTutorialContent(); +export let TUTORIAL_CONTENT = getTutorialContent(); // Listen for language changes and update content i18n.on('languageChanged', () => { - Object.assign(TUTORIAL_CONTENT, getTutorialContent()); + TUTORIAL_CONTENT = getTutorialContent(); }); -// Tutorial steps configuration -export interface TutorialStep { - id: number; - type: 'welcome' | 'spotlight' | 'completion'; - targetScreen?: 'Home' | 'Categories' | 'TaskList' | 'Notes' | 'Calendar' | 'Statistics'; - targetElement?: string; // ref name for spotlight - content: { - title: string; - description: string; - icon?: string; - }; -} +// Tutorial images - real screenshots +const TUTORIAL_IMAGES = { + homeTextChat: require('../../assets/tutorial/Text_chat.png'), + homeVoiceChat: require('../../assets/tutorial/Voice_chat.png'), + homeChatHistory: require('../../assets/tutorial/Chat_history.png'), + categoriesEditCategory: require('../../assets/tutorial/Edit_category.png'), + categoriesEditTask: require('../../assets/tutorial/Edit_task.png'), + calendarSwitch: require('../../assets/tutorial/Switch_calendar.png'), +}; +// Get tutorial steps grouped by section export const getTutorialSteps = (): TutorialStep[] => { const content = getTutorialContent(); return [ + // Home Section + { + key: 'home-text-chat', + section: 'home', + title: content.steps.homeTextChat.title, + description: content.steps.homeTextChat.description, + image: TUTORIAL_IMAGES.homeTextChat, + }, + { + key: 'home-voice-chat', + section: 'home', + title: content.steps.homeVoiceChat.title, + description: content.steps.homeVoiceChat.description, + image: TUTORIAL_IMAGES.homeVoiceChat, + }, { - id: 1, - type: 'welcome', - content: { - title: content.welcome.title, - description: content.welcome.description, - }, + key: 'home-chat-history', + section: 'home', + title: content.steps.homeChatHistory.title, + description: content.steps.homeChatHistory.description, + image: TUTORIAL_IMAGES.homeChatHistory, }, + // Categories Section { - id: 2, - type: 'spotlight', - targetScreen: 'Home', - targetElement: 'chatInput', - content: { - title: content.step2.title, - description: content.step2.description, - icon: content.step2.icon, - }, + key: 'categories-edit-category', + section: 'categories', + title: content.steps.categoriesEditCategory.title, + description: content.steps.categoriesEditCategory.description, + image: TUTORIAL_IMAGES.categoriesEditCategory, }, { - id: 3, - type: 'spotlight', - targetScreen: 'Categories', - targetElement: 'categoryList', - content: { - title: content.step3.title, - description: content.step3.description, - icon: content.step3.icon, - }, + key: 'categories-edit-task', + section: 'categories', + title: content.steps.categoriesEditTask.title, + description: content.steps.categoriesEditTask.description, + image: TUTORIAL_IMAGES.categoriesEditTask, }, + // Calendar Section { - id: 4, - type: 'spotlight', - targetScreen: 'Categories', - targetElement: 'taskItem', - content: { - title: content.step4.title, - description: content.step4.description, - icon: content.step4.icon, - }, + key: 'calendar-switch', + section: 'calendar', + title: content.steps.calendarSwitch.title, + description: content.steps.calendarSwitch.description, + image: TUTORIAL_IMAGES.calendarSwitch, }, + ]; +}; + +// Get steps organized by sections (for section headers in the tutorial) +export const getTutorialSections = (): TutorialSection[] => { + const content = getTutorialContent(); + const steps = getTutorialSteps(); + + return [ { - id: 5, - type: 'spotlight', - targetScreen: 'Notes', - targetElement: 'whiteboard', - content: { - title: content.step5.title, - description: content.step5.description, - icon: content.step5.icon, - }, + key: 'home', + title: content.sections.home, + steps: steps.filter(s => s.section === 'home'), }, { - id: 6, - type: 'spotlight', - targetScreen: 'Calendar', - targetElement: 'calendar', - content: { - title: content.step6.title, - description: content.step6.description, - icon: content.step6.icon, - }, + key: 'categories', + title: content.sections.categories, + steps: steps.filter(s => s.section === 'categories'), }, { - id: 7, - type: 'completion', - content: { - title: content.completion.title, - description: content.completion.description, - }, + key: 'calendar', + title: content.sections.calendar, + steps: steps.filter(s => s.section === 'calendar'), }, ]; }; -export const TUTORIAL_STEPS = getTutorialSteps(); - // AsyncStorage key for tutorial completion status export const TUTORIAL_STORAGE_KEY = '@mytaskly:tutorial_completed'; diff --git a/src/contexts/TutorialContext.tsx b/src/contexts/TutorialContext.tsx index c4d17b7..1853d30 100644 --- a/src/contexts/TutorialContext.tsx +++ b/src/contexts/TutorialContext.tsx @@ -3,18 +3,15 @@ import React, { useContext, useState, useCallback, - useRef, } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { TUTORIAL_STORAGE_KEY } from "../constants/tutorialContent"; export interface TutorialContextType { isTutorialVisible: boolean; - shouldAutoStart: boolean; startTutorial: () => void; closeTutorial: () => void; - registerElementRef: (key: string, ref: any) => void; - getElementRef: (key: string) => any; + skipTutorial: () => void; } const TutorialContext = createContext( @@ -25,18 +22,8 @@ export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [isTutorialVisible, setIsTutorialVisible] = useState(false); - const [shouldAutoStart, setShouldAutoStart] = useState(false); - const elementRefsMap = useRef<{ [key: string]: any }>({}); - // Log state changes - React.useEffect(() => { - console.log( - "[TUTORIAL_CONTEXT] 📊 State changed - isTutorialVisible:", - isTutorialVisible - ); - }, [isTutorialVisible]); - - // Check if tutorial should auto-start + // Check if tutorial should auto-start on first launch React.useEffect(() => { const checkTutorialStatus = async () => { try { @@ -44,8 +31,6 @@ export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({ const hasCompleted = status === "true" || status === "skipped"; if (!hasCompleted) { - // Auto-start tutorial for first-time users - setShouldAutoStart(true); setIsTutorialVisible(true); } } catch (error) { @@ -57,36 +42,29 @@ export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); const startTutorial = useCallback(() => { - console.log( - "[TUTORIAL_CONTEXT] 🎯 startTutorial called - setting isTutorialVisible to true" - ); setIsTutorialVisible(true); }, []); const closeTutorial = useCallback(() => { - console.log( - "[TUTORIAL_CONTEXT] 🔴 closeTutorial called - setting isTutorialVisible to false" - ); setIsTutorialVisible(false); }, []); - const registerElementRef = useCallback((key: string, ref: any) => { - elementRefsMap.current[key] = ref; - }, []); - - const getElementRef = useCallback((key: string) => { - return elementRefsMap.current[key]; + const skipTutorial = useCallback(async () => { + try { + await AsyncStorage.setItem(TUTORIAL_STORAGE_KEY, "skipped"); + setIsTutorialVisible(false); + } catch (error) { + console.error("[TUTORIAL] Error saving skip status:", error); + } }, []); return ( {children} @@ -100,20 +78,14 @@ export const useTutorialContext = () => { console.warn( "[TUTORIAL] useTutorialContext called outside TutorialProvider" ); - // Return a default context instead of throwing return { isTutorialVisible: false, - shouldAutoStart: false, startTutorial: () => - console.warn( - "[TUTORIAL] startTutorial called but context not available" - ), + console.warn("[TUTORIAL] startTutorial called but context not available"), closeTutorial: () => - console.warn( - "[TUTORIAL] closeTutorial called but context not available" - ), - registerElementRef: () => {}, - getElementRef: () => null, + console.warn("[TUTORIAL] closeTutorial called but context not available"), + skipTutorial: () => + console.warn("[TUTORIAL] skipTutorial called but context not available"), }; } return context; diff --git a/src/hooks/useTutorial.ts b/src/hooks/useTutorial.ts deleted file mode 100644 index 61bd3d5..0000000 --- a/src/hooks/useTutorial.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; -import { NavigationProp } from '@react-navigation/native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { TUTORIAL_STEPS, TUTORIAL_STORAGE_KEY, TutorialStep } from '../constants/tutorialContent'; -import { RootStackParamList, TabParamList } from '../navigation'; - -export interface ElementMeasurement { - x: number; - y: number; - width: number; - height: number; - pageX: number; - pageY: number; -} - -export interface UseTutorialProps { - navigation: NavigationProp | NavigationProp; - onComplete?: () => void; - onSkip?: () => void; -} - -export const useTutorial = ({ navigation, onComplete, onSkip }: UseTutorialProps) => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isVisible, setIsVisible] = useState(false); - const [targetMeasurement, setTargetMeasurement] = useState(null); - const [isNavigating, setIsNavigating] = useState(false); - - const elementRefs = useRef<{ [key: string]: any }>({}); - const navigationTimeoutRef = useRef(null); - - const currentStep = TUTORIAL_STEPS[currentStepIndex]; - - // Check if tutorial was already completed - const checkTutorialStatus = useCallback(async () => { - try { - const status = await AsyncStorage.getItem(TUTORIAL_STORAGE_KEY); - return status === 'true' || status === 'skipped'; - } catch (error) { - console.error('[TUTORIAL] Error checking tutorial status:', error); - return false; - } - }, []); - - // Start tutorial - const startTutorial = useCallback(async (force: boolean = false) => { - console.log('[TUTORIAL_HOOK] 🎯 startTutorial called with force:', force); - - if (force) { - // Force start tutorial regardless of completion status - console.log('[TUTORIAL_HOOK] 🚀 Force starting tutorial'); - setCurrentStepIndex(0); - setIsVisible(true); - return; - } - - const isCompleted = await checkTutorialStatus(); - console.log('[TUTORIAL_HOOK] 📊 Tutorial completion status:', isCompleted); - - if (!isCompleted) { - console.log('[TUTORIAL_HOOK] ✅ Starting tutorial (not completed)'); - setCurrentStepIndex(0); - setIsVisible(true); - } else { - console.log('[TUTORIAL_HOOK] ⏭️ Tutorial already completed, skipping'); - } - }, [checkTutorialStatus]); - - // Restart tutorial (for "Review Tutorial" option) - const restartTutorial = useCallback(() => { - setCurrentStepIndex(0); - setIsVisible(true); - setTargetMeasurement(null); - }, []); - - // Register element ref for spotlight - const registerElement = useCallback((elementName: string, ref: any) => { - elementRefs.current[elementName] = ref; - }, []); - - // Measure element for spotlight - const measureElement = useCallback((elementName: string): Promise => { - return new Promise((resolve) => { - const ref = elementRefs.current[elementName]; - - if (!ref || !ref.measure) { - console.warn(`[TUTORIAL] No ref found for element: ${elementName}`); - resolve(null); - return; - } - - ref.measure( - (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => { - if (width === 0 || height === 0) { - console.warn(`[TUTORIAL] Invalid measurements for ${elementName}`); - resolve(null); - return; - } - - resolve({ x, y, width, height, pageX, pageY }); - } - ); - }); - }, []); - - // Navigate to target screen and measure element - const navigateAndMeasure = useCallback(async (step: TutorialStep) => { - if (!step.targetScreen || !step.targetElement) { - setTargetMeasurement(null); - return; - } - - setIsNavigating(true); - - try { - console.log(`[TUTORIAL] 🧭 Target screen: ${step.targetScreen}`); - - // Get current navigation state - const state = navigation.getState(); - const currentRoute = state?.routes?.[state?.index]; - - console.log('[TUTORIAL] Current route:', currentRoute?.name); - - // List of tab screens that are nested inside HomeTabs - const tabScreens = ['Home', 'Categories', 'Notes', 'Calendar', 'Statistics', 'BotChat']; - - if (tabScreens.includes(step.targetScreen)) { - // This is a tab screen - use nested navigation - console.log(`[TUTORIAL] 📱 Navigating to tab: ${step.targetScreen} inside HomeTabs`); - - try { - // @ts-ignore - Navigate to HomeTabs with nested screen parameter - navigation.navigate('HomeTabs', { - screen: step.targetScreen, - }); - } catch (navError) { - console.error(`[TUTORIAL] ❌ Failed to navigate to tab ${step.targetScreen}:`, navError); - } - - // Wait longer for nested navigation to complete - await new Promise(resolve => { - navigationTimeoutRef.current = setTimeout(resolve, 600); - }); - } else { - // This is a stack screen - navigate normally - console.log(`[TUTORIAL] 📍 Navigating to stack screen: ${step.targetScreen}`); - - try { - // @ts-ignore - navigation.navigate(step.targetScreen as any); - } catch (navError) { - console.error(`[TUTORIAL] ❌ Failed to navigate to ${step.targetScreen}:`, navError); - } - - // Wait for navigation to complete - await new Promise(resolve => { - navigationTimeoutRef.current = setTimeout(resolve, 400); - }); - } - - console.log(`[TUTORIAL] 📏 Measuring element: ${step.targetElement}`); - - // Measure the target element - const measurement = await measureElement(step.targetElement); - - if (measurement) { - console.log(`[TUTORIAL] ✅ Element measured successfully:`, measurement); - } else { - console.warn(`[TUTORIAL] ⚠️ Failed to measure element: ${step.targetElement}`); - } - - setTargetMeasurement(measurement); - } catch (error) { - console.error(`[TUTORIAL] ❌ Error in navigateAndMeasure:`, error); - console.error('[TUTORIAL] Error details:', { - targetScreen: step.targetScreen, - targetElement: step.targetElement, - errorMessage: error instanceof Error ? error.message : String(error) - }); - setTargetMeasurement(null); - } finally { - setIsNavigating(false); - } - }, [navigation, measureElement]); - - // Go to next step - const nextStep = useCallback(async () => { - if (currentStepIndex < TUTORIAL_STEPS.length - 1) { - const nextIndex = currentStepIndex + 1; - const nextStepData = TUTORIAL_STEPS[nextIndex]; - - setCurrentStepIndex(nextIndex); - - if (nextStepData.type === 'spotlight') { - await navigateAndMeasure(nextStepData); - } else { - setTargetMeasurement(null); - } - } - }, [currentStepIndex, navigateAndMeasure]); - - // Go to previous step - const previousStep = useCallback(async () => { - if (currentStepIndex > 0) { - const prevIndex = currentStepIndex - 1; - const prevStepData = TUTORIAL_STEPS[prevIndex]; - - setCurrentStepIndex(prevIndex); - - if (prevStepData.type === 'spotlight') { - await navigateAndMeasure(prevStepData); - } else { - setTargetMeasurement(null); - } - } - }, [currentStepIndex, navigateAndMeasure]); - - // Skip tutorial - const skipTutorial = useCallback(async () => { - try { - await AsyncStorage.setItem(TUTORIAL_STORAGE_KEY, 'skipped'); - setIsVisible(false); - setTargetMeasurement(null); - - // Navigate back to Home tab using nested navigation - // @ts-ignore - navigation.navigate('HomeTabs', { screen: 'Home' }); - - onSkip?.(); - } catch (error) { - console.error('[TUTORIAL] Error skipping tutorial:', error); - } - }, [navigation, onSkip]); - - // Complete tutorial - const completeTutorial = useCallback(async () => { - try { - await AsyncStorage.setItem(TUTORIAL_STORAGE_KEY, 'true'); - setIsVisible(false); - setTargetMeasurement(null); - - // Navigate back to Home tab using nested navigation - // @ts-ignore - navigation.navigate('HomeTabs', { screen: 'Home' }); - - onComplete?.(); - } catch (error) { - console.error('[TUTORIAL] Error completing tutorial:', error); - } - }, [navigation, onComplete]); - - // Effect to handle step changes and measurements - useEffect(() => { - if (isVisible && currentStep && currentStep.type === 'spotlight') { - navigateAndMeasure(currentStep); - } - }, [currentStep, isVisible, navigateAndMeasure]); - - // Cleanup on unmount - useEffect(() => { - return () => { - if (navigationTimeoutRef.current) { - clearTimeout(navigationTimeoutRef.current); - } - }; - }, []); - - return { - // State - isVisible, - currentStep, - currentStepIndex, - totalSteps: TUTORIAL_STEPS.length, - targetMeasurement, - isNavigating, - - // Actions - startTutorial, - restartTutorial, - nextStep, - previousStep, - skipTutorial, - completeTutorial, - registerElement, - - // Helpers - isFirstStep: currentStepIndex === 0, - isLastStep: currentStepIndex === TUTORIAL_STEPS.length - 1, - canGoBack: currentStepIndex > 0, - canGoNext: currentStepIndex < TUTORIAL_STEPS.length - 1, - }; -}; diff --git a/src/hooks/useTutorialElement.ts b/src/hooks/useTutorialElement.ts deleted file mode 100644 index 94d1f23..0000000 --- a/src/hooks/useTutorialElement.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useRef, useEffect } from 'react'; -import { useTutorialContext } from '../contexts/TutorialContext'; - -/** - * Custom hook to easily register an element for tutorial spotlight - * - * @param elementKey - Unique key to identify this element in the tutorial - * @returns ref - Ref to attach to your component - * - * @example - * ```typescript - * const chatInputRef = useTutorialElement('chatInput'); - * - * return ( - * - * ); - * ``` - */ -export const useTutorialElement = (elementKey: string) => { - const { registerElementRef } = useTutorialContext(); - const elementRef = useRef(null); - - useEffect(() => { - if (elementKey && elementRef.current) { - registerElementRef(elementKey, elementRef.current); - } - }, [elementKey, registerElementRef]); - - return elementRef; -}; diff --git a/src/locales/en.json b/src/locales/en.json index 7ff28ad..2f05480 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -50,7 +50,14 @@ "voiceSettings": "Voice settings", "googleCalendar": "Google Calendar", "about": "About", + "tutorial": "Review Tutorial", "testNotifications": "Test Notifications (10s)" + }, + "tutorial": { + "restartTitle": "Tutorial", + "restartMessage": "The tutorial has been restarted! Go back to the main screen to begin.", + "restartError": "Error", + "restartErrorMessage": "Unable to restart the tutorial. Please try again later." } }, "home": { @@ -77,30 +84,41 @@ "startButton": "Start Tour", "skipButton": "Skip tour" }, + "sections": { + "home": "Home", + "categories": "Categories", + "calendar": "Calendar" + }, "steps": { - "chat": { - "title": "Chat with AI", - "description": "Write a message or tap the microphone to chat vocally. The AI assistant will help you manage your activities." + "home": { + "textChat": { + "title": "Text Chat", + "description": "Type a message in the text field at the bottom of the screen to chat with the AI assistant. You can ask to create tasks, organize activities or get suggestions." + }, + "voiceChat": { + "title": "Voice Chat", + "description": "Tap the microphone icon next to the text field to open the voice chat. Speak directly with the AI assistant to create tasks and manage your activities hands-free." + }, + "chatHistory": { + "title": "Chat History", + "description": "Tap the chat bubble icon in the top bar to access the history of your previous conversations. You can review or continue past chats." + } }, "categories": { - "title": "Your Categories", - "description": "Organize your activities in categories. Long press on a category to edit or delete it." - }, - "tasks": { - "title": "Manage Tasks", - "description": "Tap a task to complete it. Long press to edit, delete or set reminders." - }, - "notes": { - "title": "Draw and Annotate", - "description": "Use the whiteboard to draw, take notes or create sketches. Tap the + button to add a new note." + "editCategory": { + "title": "Edit a Category", + "description": "Long press on a category card to open the edit menu. You can rename the category, change its color or delete it." + }, + "editTask": { + "title": "Edit a Task", + "description": "Tap on a task to open its details. From there you can edit the title, description, due date, priority and more. Swipe left for quick actions." + } }, "calendar": { - "title": "Activity Calendar", - "description": "View all your tasks organized by date. Tap a day to see scheduled activities." - }, - "statistics": { - "title": "Your Statistics", - "description": "Monitor your progress with charts and analytics. View the number of completed tasks, category distribution and your trend over time." + "switch": { + "title": "Switch Calendar View", + "description": "Tap the view toggle button in the top right corner to switch between the basic calendar view and the advanced calendar view with more options." + } } }, "completion": { diff --git a/src/locales/it.json b/src/locales/it.json index 55bdd38..0942540 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -50,7 +50,14 @@ "voiceSettings": "Impostazioni vocali", "googleCalendar": "Google Calendar", "about": "Info", + "tutorial": "Rivedi Tutorial", "testNotifications": "Test Notifiche (10s)" + }, + "tutorial": { + "restartTitle": "Tutorial", + "restartMessage": "Il tutorial è stato riavviato! Torna alla schermata principale per iniziare.", + "restartError": "Errore", + "restartErrorMessage": "Impossibile riavviare il tutorial. Riprova più tardi." } }, "home": { @@ -73,34 +80,45 @@ "tutorial": { "welcome": { "title": "Benvenuto in MyTaskly!", - "description": "Il tuo assistente AI personale per gestire task, note e calendario. Scopri come usare tutte le funzionalità in pochi passaggi.", + "description": "Il tuo assistente AI personale per gestire task, note e calendario. Scopri come usare tutte le funzionalita' in pochi passaggi.", "startButton": "Inizia il Tour", "skipButton": "Salta il tour" }, + "sections": { + "home": "Home", + "categories": "Categorie", + "calendar": "Calendario" + }, "steps": { - "chat": { - "title": "Chat con l'AI", - "description": "Scrivi un messaggio o tocca il microfono per chattare vocalmente. L'assistente AI ti aiuterà a gestire le tue attività." + "home": { + "textChat": { + "title": "Chat Testuale", + "description": "Scrivi un messaggio nel campo di testo in basso per chattare con l'assistente AI. Puoi chiedere di creare task, organizzare attivita' o ricevere suggerimenti." + }, + "voiceChat": { + "title": "Chat Vocale", + "description": "Tocca l'icona del microfono accanto al campo di testo per aprire la chat vocale. Parla direttamente con l'assistente AI per creare task e gestire le tue attivita' a mani libere." + }, + "chatHistory": { + "title": "Cronologia Chat", + "description": "Tocca l'icona della chat nella barra in alto per accedere alla cronologia delle conversazioni precedenti. Puoi rivedere o continuare le chat passate." + } }, "categories": { - "title": "Le tue Categorie", - "description": "Organizza le tue attività in categorie. Tieni premuto su una categoria per modificarla o eliminarla." - }, - "tasks": { - "title": "Gestisci i Task", - "description": "Tocca un task per completarlo. Tieni premuto per modificare, eliminare o impostare promemoria." - }, - "notes": { - "title": "Disegna e Annota", - "description": "Usa la lavagna per disegnare, prendere appunti o creare schizzi. Tocca il pulsante + per aggiungere una nuova nota." + "editCategory": { + "title": "Modificare una Categoria", + "description": "Tieni premuto su una card della categoria per aprire il menu di modifica. Puoi rinominarla, cambiare il colore o eliminarla." + }, + "editTask": { + "title": "Modificare un Task", + "description": "Tocca un task per aprire i suoi dettagli. Da li' puoi modificare titolo, descrizione, scadenza, priorita' e altro. Scorri a sinistra per le azioni rapide." + } }, "calendar": { - "title": "Calendario delle Attività", - "description": "Visualizza tutti i tuoi task organizzati per data. Tocca un giorno per vedere le attività programmate." - }, - "statistics": { - "title": "Le tue Statistiche", - "description": "Monitora il tuo progresso con grafici e analitiche. Visualizza il numero di task completati, la distribuzione per categoria e il tuo andamento nel tempo." + "switch": { + "title": "Cambia Vista Calendario", + "description": "Tocca il pulsante di cambio vista in alto a destra per passare dalla vista calendario base a quella avanzata con piu' opzioni." + } } }, "completion": { diff --git a/src/navigation/index.tsx b/src/navigation/index.tsx index 52e526a..06d45f5 100644 --- a/src/navigation/index.tsx +++ b/src/navigation/index.tsx @@ -39,8 +39,8 @@ import { syncAllData } from "../services/taskService"; import { handleGoogleLoginSuccess, handleGoogleLoginError } from "../services/googleSignInService"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { STORAGE_KEYS } from "../constants/authConstants"; -import { TutorialProvider } from "../contexts/TutorialContext"; -import { TutorialManager } from "../components/Tutorial"; +import { TutorialProvider, useTutorialContext } from "../contexts/TutorialContext"; +import { TutorialOnboarding } from "../components/Tutorial/exports"; import { LanguageProvider } from "../contexts/LanguageContext"; import "../services/i18n"; // Initialize i18n import { useTranslation } from 'react-i18next'; @@ -85,7 +85,6 @@ const Tab = createBottomTabNavigator(); // Tab Navigator per le schermate principali function HomeTabs() { - const navigation = useNavigation>(); const { t } = useTranslation(); return ( @@ -150,9 +149,6 @@ function HomeTabs() { options={{ title: t('navigation.tabs.statistics') }} /> */} - - {/* Tutorial Manager */} - ); } @@ -469,8 +465,22 @@ export default function Navigation() { + ); } + +// Wrapper that connects TutorialOnboarding to the TutorialContext +function TutorialOnboardingWrapper() { + const { isTutorialVisible, closeTutorial, skipTutorial } = useTutorialContext(); + + return ( + + ); +} diff --git a/src/navigation/screens/Calendar.tsx b/src/navigation/screens/Calendar.tsx index 0b4ecc7..9d8580e 100644 --- a/src/navigation/screens/Calendar.tsx +++ b/src/navigation/screens/Calendar.tsx @@ -64,16 +64,16 @@ export default function Calendar() { {t('calendar.title')} - - + onPress={toggleViewMode} + style={styles.toggleButton} + activeOpacity={0.7} + > + + diff --git a/src/navigation/screens/Categories.tsx b/src/navigation/screens/Categories.tsx index 3d30ebc..2c81935 100644 --- a/src/navigation/screens/Categories.tsx +++ b/src/navigation/screens/Categories.tsx @@ -69,8 +69,8 @@ export default function Categories() { style={styles.searchButton} /> - - + + diff --git a/src/navigation/screens/Home.tsx b/src/navigation/screens/Home.tsx index 2572d40..c2dc665 100644 --- a/src/navigation/screens/Home.tsx +++ b/src/navigation/screens/Home.tsx @@ -28,12 +28,13 @@ import { TaskCacheService } from '../../services/TaskCacheService'; import SyncManager, { SyncStatus } from '../../services/SyncManager'; import Badge from "../../components/UI/Badge"; import VoiceChatModal from "../../components/BotChat/VoiceChatModal"; -import { useTutorialContext } from "../../contexts/TutorialContext"; import { useTranslation } from 'react-i18next'; import { ChatHistory } from "../../components/BotChat/ChatHistory"; +import { useTutorialContext } from "../../contexts/TutorialContext"; const HomeScreen = () => { const { t } = useTranslation(); + const { startTutorial } = useTutorialContext(); const [message, setMessage] = useState(""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [chatStarted, setChatStarted] = useState(false); const [userName, setUserName] = useState("Utente"); @@ -48,26 +49,10 @@ const HomeScreen = () => { const [showChatHistory, setShowChatHistory] = useState(false); const [currentChatId, setCurrentChatId] = useState(null); - // Tutorial context - const tutorialContext = useTutorialContext(); - // Costanti const USER = 'user'; const BOT = 'bot'; - const handleStartTutorial = () => { - console.log('[HOME] Tutorial context:', tutorialContext); - console.log('[HOME] startTutorial function:', tutorialContext?.startTutorial); - - if (tutorialContext && tutorialContext.startTutorial) { - console.log('[HOME] 🎯 Starting tutorial...'); - tutorialContext.startTutorial(); - } else { - console.error('[HOME] ❌ Tutorial context not available!'); - Alert.alert('Errore', 'Tutorial context non disponibile'); - } - }; - // Servizi const cacheService = useRef(TaskCacheService.getInstance()).current; const syncManager = useRef(SyncManager.getInstance()).current; @@ -629,6 +614,17 @@ const HomeScreen = () => { } }; + const handleStartTutorial = async () => { + try { + // Clear tutorial completion status to allow restart + await AsyncStorage.removeItem('@mytaskly:tutorial_completed'); + // Start the tutorial + startTutorial(); + } catch (error) { + console.error('[HOME] Error starting tutorial:', error); + } + }; + // Calcolo dinamico del padding top basato sull'altezza dello schermo const getGreetingPaddingTop = () => { if (screenHeight < 700) return Math.max(screenHeight * 0.15, 80); // Schermi piccoli @@ -696,28 +692,32 @@ const HomeScreen = () => { )} - {/* TEMPORARY: Start Tutorial Button */} + {/* Tutorial Help Button */} - - - - {/* Chat History Toggle Button */} - + {/* Chat History Toggle Button */} + + + + {chatStarted && !showChatHistory && ( { - + + + + + + { + console.log('[HOME] TextInput focused (under greeting)'); + setIsInputFocused(true); + }} + onBlur={() => { + console.log('[HOME] TextInput blurred (under greeting)'); + setIsInputFocused(false); + }} + /> - + - - { - console.log('[HOME] TextInput focused (under greeting)'); - setIsInputFocused(true); - }} - onBlur={() => { - console.log('[HOME] TextInput blurred (under greeting)'); - setIsInputFocused(false); - }} - /> - - - - + {/* Comando suggerito */} diff --git a/src/navigation/screens/Notes.tsx b/src/navigation/screens/Notes.tsx index 4829877..6cadfbe 100644 --- a/src/navigation/screens/Notes.tsx +++ b/src/navigation/screens/Notes.tsx @@ -56,14 +56,14 @@ const NotesContent: React.FC = () => { - - - - + + + + ); }; diff --git a/src/navigation/screens/Settings.tsx b/src/navigation/screens/Settings.tsx index 30e2374..17f65ea 100644 --- a/src/navigation/screens/Settings.tsx +++ b/src/navigation/screens/Settings.tsx @@ -1,15 +1,19 @@ import { Text } from '@react-navigation/elements'; import React from 'react'; -import { StyleSheet, View, TouchableOpacity, SafeAreaView, StatusBar, ScrollView } from 'react-native'; +import { StyleSheet, View, TouchableOpacity, SafeAreaView, StatusBar, ScrollView, Alert } from 'react-native'; import { useNavigation, NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../types'; import { Ionicons } from '@expo/vector-icons'; import axiosInstance from '../../services/axiosInstance'; import { useTranslation } from 'react-i18next'; +import { useTutorialContext } from '../../contexts/TutorialContext'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { TUTORIAL_STORAGE_KEY } from '../../constants/tutorialContent'; export default function Settings() { const navigation = useNavigation>(); const { t } = useTranslation(); + const { startTutorial } = useTutorialContext(); const handleNavigateToAccountSettings = () => { navigation.navigate('AccountSettings'); @@ -43,6 +47,23 @@ export default function Settings() { navigation.navigate('CalendarWidgetDemo'); }; + const handleRestartTutorial = async () => { + try { + // Remove tutorial completion flag to allow restart + await AsyncStorage.removeItem(TUTORIAL_STORAGE_KEY); + + // Start the tutorial + startTutorial(); + } catch (error) { + console.error('[Settings] Error restarting tutorial:', error); + Alert.alert( + t('settings.tutorial.restartError'), + t('settings.tutorial.restartErrorMessage'), + [{ text: t('common.buttons.ok') }] + ); + } + }; + const testNotification = async () => { try { const response = await axiosInstance.post('api/notifications/test-timer-notification', { @@ -157,6 +178,17 @@ export default function Settings() { + + + + {t('settings.menu.tutorial')} + + + + {/* Development Section */} {t('settings.sections.development')}