diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fd6c81f..02a8845 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug Report -about: Create a report to help us improve LevelUp +about: Create a report to help us improve ClimbStack title: '[BUG] ' labels: bug assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index dc4d231..290fff8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- name: Feature Request -about: Suggest an idea for LevelUp +about: Suggest an idea for ClimbStack title: '[FEAT] ' labels: enhancement assignees: '' @@ -12,7 +12,7 @@ assignees: '' ## Describe the solution you'd like - + ## Describe alternatives you've considered diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index be3d146..e7fb3a9 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -43,7 +43,7 @@ jobs: - name: 📂 Upload APK Artifact uses: actions/upload-artifact@v7 with: - name: LevelUp-APK + name: ClimbStack-APK path: android/app/build/outputs/apk/release/app-release.apk - name: 🚀 Create GitHub Release @@ -51,5 +51,5 @@ jobs: uses: softprops/action-gh-release@v3 with: files: android/app/build/outputs/apk/release/app-release.apk - name: LevelUp ${{ github.ref_name }} + name: ClimbStack ${{ github.ref_name }} generate_release_notes: true diff --git a/AUTHORS b/AUTHORS index 1e24ca1..336158f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,4 +1,4 @@ -All people who contributed to LevelUp by sending at least a patch or +All people who contributed to ClimbStack by sending at least a patch or more (in the order of the date of their first contribution), except those who explicitly didn't want to be mentioned. People with a * next to their names are not found in the metadata of the git history. diff --git a/CITATION.cff b/CITATION.cff index 62d32ae..6904ec9 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,12 +4,12 @@ authors: - family-names: 'Zanotti' given-names: 'Giovanni' orcid: 'https://orcid.org/0009-0000-8900-9586' -title: 'LevelUp: Defeating Decision Fatigue via Gamified Time Management' -version: 1.2.0 +title: 'ClimbStack: Defeating Decision Fatigue via Gamified Time Management' +version: 1.3.0 date-released: '2026-09-25' -url: 'https://github.com/GiZano/LevelUp' -repository-code: 'https://github.com/GiZano/LevelUp' -abstract: 'LevelUp is an offline, privacy-first React Native application that leverages Agent-Driven Development to bridge the gap between long-term ambitions (Peaks) and daily execution (Weekly Block Planner).' +url: 'https://github.com/ClimbStack/climbstack-app' +repository-code: 'https://github.com/ClimbStack/climbstack-app' +abstract: 'ClimbStack is an offline, privacy-first React Native application that leverages Agent-Driven Development to bridge the gap between long-term ambitions (Peaks) and daily execution (Weekly Block Planner).' keywords: - time-management - react-native @@ -20,4 +20,4 @@ license: MIT identifiers: - type: doi value: '10.5281/zenodo.22969593' - description: 'Zenodo archive of the LevelUp source code' + description: 'Zenodo archive of the ClimbStack source code' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ba0e3e..80a0e9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to LevelUp 🏔️ +# Contributing to ClimbStack 🏔️ -First off, thank you for considering contributing to LevelUp! It's people like you that make the open-source community such a great place to learn, inspire, and create. +First off, thank you for considering contributing to ClimbStack! It's people like you that make the open-source community such a great place to learn, inspire, and create. ## How to Contribute @@ -9,7 +9,7 @@ First off, thank you for considering contributing to LevelUp! It's people like y 3. **Develop**: Write your code. Make sure to adhere to the coding guidelines below. 4. **Test**: Ensure the app builds locally and your changes don't break existing functionality. 5. **Commit**: Use Conventional Commits (e.g., `feat: add new block type`, `fix: resolve crash on peaks screen`). -6. **Push & PR**: Push your branch to your fork and open a Pull Request against the `main` branch of LevelUp. +6. **Push & PR**: Push your branch to your fork and open a Pull Request against the `main` branch of ClimbStack. ## Local Development Setup @@ -49,7 +49,7 @@ If you are new to the codebase, look for issues labeled `good first issue` or `h ## Modular Architecture -LevelUp strictly follows a modular architecture (inspired by large-scale projects like SymPy). When contributing, please ensure your code is isolated to its specific domain: +ClimbStack strictly follows a modular architecture (inspired by large-scale projects like SymPy). When contributing, please ensure your code is isolated to its specific domain: - `src/store/`: Core logic, SQLite interactions, and React Context state. - `src/components/`: Dumb, reusable UI components (e.g., SVG charts, buttons). @@ -59,7 +59,7 @@ LevelUp strictly follows a modular architecture (inspired by large-scale project ## Versioning Policy -LevelUp follows [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`): +ClimbStack follows [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`): | Bump | When | Example | | ------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | diff --git a/README.md b/README.md index 728e648..a5ddd80 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- LevelUp Logo -

LevelUp

+ ClimbStack Logo +

ClimbStack

Gamify your goals. Conquer your peaks.

@@ -10,12 +10,12 @@ SQLite Open Source DOI - Codecov + Codecov

An open-source productivity engine, built by developers, for everyone.

-LevelUp is a mobile app for long-term goal tracking, built around the metaphor of **mountain climbing**: every goal is a peak to reach, every sub-goal is a base camp along the way. It includes a weekly planner to organize your week in blocks and eliminate _decision fatigue_. +ClimbStack is a mobile app for long-term goal tracking, built around the metaphor of **mountain climbing**: every goal is a peak to reach, every sub-goal is a base camp along the way. It includes a weekly planner to organize your week in blocks and eliminate _decision fatigue_. > [!NOTE] > Personal project born to solve a real problem: not knowing what to do in free time and ending up scrolling the phone. The app is the scaffolding — the real goal is to build the time-boxing habit until it becomes natural. @@ -69,8 +69,8 @@ LevelUp is a mobile app for long-term goal tracking, built around the metaphor o ```bash # Clone the repository -git clone https://github.com/gizano/LevelUp.git -cd LevelUp +git clone https://github.com/ClimbStack/climbstack-app.git +cd climbstack-app # Install dependencies npm install @@ -91,7 +91,7 @@ eas build --platform android --profile preview ### Installation from Release -1. Go to the [Releases](https://github.com/gizano/LevelUp/releases) page of the repository +1. Go to the [Releases](https://github.com/ClimbStack/climbstack-app/releases) page of the repository 2. Download the APK of the latest release 3. Transfer it to your phone and install it (enable "Install from unknown sources" if asked) @@ -100,7 +100,7 @@ eas build --platform android --profile preview ## 📁 Project Structure ``` -LevelUp/ +climbstack-app/ ├── src/ │ ├── components/ # Reusable UI components │ ├── screens/ # App screens @@ -165,8 +165,8 @@ See [ROADMAP.md](./ROADMAP.md) for the complete development plan. We are actively working to cover the entire application with automated tests. Help us make this sunburst graph completely green!

- - Codecov Sunburst + + Codecov Sunburst

@@ -174,7 +174,7 @@ We are actively working to cover the entire application with automated tests. He ## 🤝 Community & Security -- **Open Source Philosophy**: LevelUp is 100% open-source, built by developers for everyone. We believe in transparency and community-driven tools. +- **Open Source Philosophy**: ClimbStack is 100% open-source, built by developers for everyone. We believe in transparency and community-driven tools. - **Code of Conduct**: Please refer to our [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) to understand the standards for participating in our community. - **Security**: If you discover any security vulnerabilities, please check our [SECURITY.md](./SECURITY.md) for reporting guidelines. diff --git a/ROADMAP.md b/ROADMAP.md index ec2c96a..04c0f55 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# 🏔️ LevelUp — Roadmap +# 🏔️ ClimbStack — Roadmap > Personal side project. Timeboxed, AI-assisted, controlled scope. > The goal is to have a working app on the phone, not to win a design award. @@ -23,7 +23,7 @@ - [x] Linter + formatter setup (ESLint + Prettier) - [x] First commit with working empty skeleton -**Deliverable**: `npx expo start` works, app shows an empty screen with "LevelUp" name +**Deliverable**: `npx expo start` works, app shows an empty screen with "ClimbStack" name --- diff --git a/SECURITY.md b/SECURITY.md index 7a23597..c9eb275 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported Versions -LevelUp currently supports the following versions with security updates: +ClimbStack currently supports the following versions with security updates: | Version | Supported | | ------- | ------------------ | @@ -11,7 +11,7 @@ LevelUp currently supports the following versions with security updates: ## Reporting a Vulnerability -Security is a priority for LevelUp. If you discover a security vulnerability, please do not disclose it publicly. +Security is a priority for ClimbStack. If you discover a security vulnerability, please do not disclose it publicly. Instead, please send an email to the project maintainer at [your.email@example.com] (or use GitHub Security Advisories) to report the vulnerability. diff --git a/app.json b/app.json index c9ee813..2d728af 100644 --- a/app.json +++ b/app.json @@ -1,8 +1,8 @@ { "expo": { - "name": "LevelUp", - "slug": "levelup", - "version": "1.2.0", + "name": "ClimbStack", + "slug": "climbstack", + "version": "1.3.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", @@ -13,7 +13,7 @@ "android": { "softwareKeyboardLayoutMode": "pan", "package": "com.gizano.levelup", - "versionCode": 5, + "versionCode": 6, "adaptiveIcon": { "backgroundColor": "#071A2F", "foregroundImage": "./assets/icon.png" @@ -34,7 +34,16 @@ } ], "expo-sharing", - "expo-splash-screen" + "expo-splash-screen", + [ + "expo-build-properties", + { + "android": { + "enableMinifyInReleaseBuilds": true, + "enableShrinkResourcesInReleaseBuilds": true + } + } + ] ], "extra": { "eas": { diff --git a/assets/tutorial_peaks.png b/assets/tutorial_peaks.png index 05565b8..06b40c0 100644 Binary files a/assets/tutorial_peaks.png and b/assets/tutorial_peaks.png differ diff --git a/assets/tutorial_settings.png b/assets/tutorial_settings.png index 87d6d4a..e37a811 100644 Binary files a/assets/tutorial_settings.png and b/assets/tutorial_settings.png differ diff --git a/docs/tutorial.pdf b/docs/tutorial.pdf index a3567c2..817d60a 100644 Binary files a/docs/tutorial.pdf and b/docs/tutorial.pdf differ diff --git a/docs/tutorial.typ b/docs/tutorial.typ index ad410f0..1fadcf1 100644 --- a/docs/tutorial.typ +++ b/docs/tutorial.typ @@ -3,7 +3,7 @@ #set par(justify: true) #align(center)[ - #text(size: 28pt, weight: "bold")[LevelUp] + #text(size: 28pt, weight: "bold")[ClimbStack] #v(1em) #text(size: 16pt)[User Manual & Full Tutorial] @@ -16,15 +16,15 @@ #v(4em) = 1. Introduction -LevelUp is a React Native application built to defeat *decision fatigue*. It combines gamified long-term goals with strict weekly time-blocking. Instead of constantly wondering "What should I do today?", LevelUp provides a clear, actionable dashboard based on your predefined weekly routine. +ClimbStack is a React Native application built to defeat *decision fatigue*. It combines gamified long-term goals with strict weekly time-blocking. Instead of constantly wondering "What should I do today?", ClimbStack provides a clear, actionable dashboard based on your predefined weekly routine. This document serves as both a comprehensive guide for new users during testing sessions and a technical reference for contributors. #pagebreak() -= 2. Quick Start: The LevelUp Methodology += 2. Quick Start: The ClimbStack Methodology -The core philosophy of LevelUp is bridging the gap between grand ambitions and daily execution. To do this, we use the mountain metaphor. +The core philosophy of ClimbStack is bridging the gap between grand ambitions and daily execution. To do this, we use the mountain metaphor. == 2.1 Peaks & Camps (The "What") A *Peak* represents a major, long-term goal (e.g., "Pass the OS Exam", "Launch a Startup"). Because Peaks are daunting, they must be broken down into *Camps* (milestones). Every time a camp is reached, you gain 100 meters of altitude, creating a gamified sense of progression. @@ -74,7 +74,7 @@ To reach camps, you need to put in the hours consistently. *Blocks* are schedule = 4. Gamification & Analytics -LevelUp uses subtle gamification to keep you engaged: +ClimbStack uses subtle gamification to keep you engaged: - *Altitude*: Earned by completing Camps (100m) and Blocks (1m/hour). Tap the "Total Altitude" card on the Home screen to view a historical breakdown of meters climbed per category and per peak. - *Peaks Reached*: Tracks fully completed goals. Tap this card to see a list of completed and in-progress peaks with exact completion percentages. - *Streaks*: Your consecutive days of activity. Do at least one block a day to keep the fire alive! @@ -83,7 +83,7 @@ LevelUp uses subtle gamification to keep you engaged: = 5. Settings & Data Management -LevelUp is fully offline and privacy-first. +ClimbStack is fully offline and privacy-first. - *Backup*: From the Settings tab, you can export all your data (Peaks, Blocks, Categories) into a single `.json` file and share it via Google Drive or Email. You can also import a backup to restore your data on a new device. - *Localization*: Change the language (English/Italian) on the fly without restarting the app. @@ -91,7 +91,7 @@ LevelUp is fully offline and privacy-first. = 6. Technical Architecture -LevelUp is designed to be fully offline, privacy-first, and lightning-fast. +ClimbStack is designed to be fully offline, privacy-first, and lightning-fast. - *Frontend Framework*: React Native (via Expo Managed Workflow). - *Language*: TypeScript. @@ -100,6 +100,6 @@ LevelUp is designed to be fully offline, privacy-first, and lightning-fast. = 7. Open Source Contribution -LevelUp strongly embraces *Agent-Driven Development*. We encourage contributors to utilize AI tools (like Copilot, Claude, or Antigravity) to write boilerplate, fix bugs, and scaffold features. +ClimbStack strongly embraces *Agent-Driven Development*. We encourage contributors to utilize AI tools (like Copilot, Claude, or Antigravity) to write boilerplate, fix bugs, and scaffold features. Please read the `AGENTS.md` and `CONTRIBUTING.md` files in the repository root for specific instructions on how to prompt your AI assistant and structure your Pull Requests for this codebase. diff --git a/package-lock.json b/package-lock.json index 361dfcc..bad5edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "levelup", - "version": "1.2.0", + "name": "climbstack", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "levelup", - "version": "1.2.0", + "name": "climbstack", + "version": "1.3.0", "dependencies": { "@expo-google-fonts/outfit": "^0.4.3", "@expo/vector-icons": "^15.0.2", @@ -16,6 +16,7 @@ "@react-navigation/native": "^7.4.1", "@react-navigation/native-stack": "^7.20.0", "expo": "~57.0.26", + "expo-build-properties": "~57.0.22", "expo-calendar": "~57.0.5", "expo-crypto": "~57.0.3", "expo-document-picker": "~57.0.3", @@ -7136,6 +7137,20 @@ "react-native": "*" } }, + "node_modules/expo-build-properties": { + "version": "57.0.22", + "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-57.0.22.tgz", + "integrity": "sha512-A5oRf2I02XDRu2NnqKo7Au5kUu244unvyoRMUsB+xvg/BOlsR+G/oyDFGkjbda1cJbeKm9eqposIXbA2ri5UrQ==", + "license": "MIT", + "dependencies": { + "@expo/schema-utils": "^57.0.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-calendar": { "version": "57.0.5", "resolved": "https://registry.npmjs.org/expo-calendar/-/expo-calendar-57.0.5.tgz", diff --git a/package.json b/package.json index cdb91ae..2e18840 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "levelup", - "version": "1.2.0", + "name": "climbstack", + "version": "1.3.0", "main": "index.ts", "overrides": { "uuid": ">=11.1.1" @@ -14,6 +14,7 @@ "@react-navigation/native": "^7.4.1", "@react-navigation/native-stack": "^7.20.0", "expo": "~57.0.26", + "expo-build-properties": "~57.0.22", "expo-calendar": "~57.0.5", "expo-crypto": "~57.0.3", "expo-document-picker": "~57.0.3", diff --git a/src/components/.gitkeep b/src/components/.gitkeep index 2ede14c..e03b342 100644 --- a/src/components/.gitkeep +++ b/src/components/.gitkeep @@ -1 +1 @@ -// LevelUp UI components +// ClimbStack UI components diff --git a/src/screens/.gitkeep b/src/screens/.gitkeep index 2ad2776..b8b96ea 100644 --- a/src/screens/.gitkeep +++ b/src/screens/.gitkeep @@ -1 +1 @@ -// LevelUp screens +// ClimbStack screens diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index b5f8ef1..7641af2 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -46,7 +46,9 @@ export default function HomeScreen({ navigation }: HomeProps) { const loadAltStats = async () => { try { const keys = await AsyncStorage.getAllKeys(); - const weekKeys = keys.filter((k: string) => k.startsWith('@levelup/week/')); + const weekKeys = keys.filter( + (k: string) => k.startsWith('@climbstack/week/') || k.startsWith('@levelup/week/') + ); const pairs = await AsyncStorage.multiGet(weekKeys); const catTotals: Record = {}; @@ -113,7 +115,10 @@ export default function HomeScreen({ navigation }: HomeProps) { React.useEffect(() => { const checkOnboarding = async () => { try { - const done = await AsyncStorage.getItem('@levelup/onboarding_done'); + let done = await AsyncStorage.getItem('@climbstack/onboarding_done'); + if (!done) { + done = await AsyncStorage.getItem('@levelup/onboarding_done'); + } if (!done) { setShowOnboarding(true); } @@ -126,7 +131,7 @@ export default function HomeScreen({ navigation }: HomeProps) { const handleDismissOnboarding = async () => { try { - await AsyncStorage.setItem('@levelup/onboarding_done', 'true'); + await AsyncStorage.setItem('@climbstack/onboarding_done', 'true'); } catch (e) { console.log(e); } @@ -135,7 +140,7 @@ export default function HomeScreen({ navigation }: HomeProps) { useLayoutEffect(() => { navigation.setOptions({ - title: 'LevelUp', + title: 'ClimbStack', headerStyle: { backgroundColor: colors.surface }, headerTintColor: colors.text, }); diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 7fe9aba..6fc6e9f 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -44,8 +44,8 @@ export default function SettingsScreen() { const exportBackup = async () => { try { const keys = await AsyncStorage.getAllKeys(); - const levelUpKeys = keys.filter((k) => k.startsWith('@levelup/')); - const pairs = await AsyncStorage.multiGet(levelUpKeys); + const appKeys = keys.filter((k) => k.startsWith('@climbstack/') || k.startsWith('@levelup/')); + const pairs = await AsyncStorage.multiGet(appKeys); const backupData = { version: 1, exportedAt: new Date().toISOString(), @@ -60,7 +60,7 @@ export default function SettingsScreen() { if (permissions.granted) { const uri = await FileSystem.StorageAccessFramework.createFileAsync( permissions.directoryUri, - 'levelup_backup.json', + 'climbstack_backup.json', 'application/json' ); await FileSystem.writeAsStringAsync(uri, jsonStr, { @@ -72,13 +72,13 @@ export default function SettingsScreen() { } // Fallback to sharing for iOS or if user cancels SAF on Android - const fileUri = FileSystem.documentDirectory + 'levelup_backup.json'; + const fileUri = FileSystem.documentDirectory + 'climbstack_backup.json'; await FileSystem.writeAsStringAsync(fileUri, jsonStr, { encoding: FileSystem.EncodingType.UTF8, }); await Sharing.shareAsync(fileUri, { mimeType: 'application/json', - dialogTitle: 'LevelUp Backup', + dialogTitle: 'ClimbStack Backup', }); } catch { Alert.alert(t('settings.exportErrorTitle'), t('settings.exportErrorMsg')); @@ -116,9 +116,11 @@ export default function SettingsScreen() { ][]; const allKeys = await AsyncStorage.getAllKeys(); - const levelUpKeys = allKeys.filter((k) => k.startsWith('@levelup/')); - if (levelUpKeys.length > 0) { - await AsyncStorage.multiRemove(levelUpKeys); + const appKeys = allKeys.filter( + (k) => k.startsWith('@climbstack/') || k.startsWith('@levelup/') + ); + if (appKeys.length > 0) { + await AsyncStorage.multiRemove(appKeys); } await AsyncStorage.multiSet(entries); @@ -277,7 +279,7 @@ export default function SettingsScreen() { Linking.openURL('https://github.com/gizano/LevelUp')} + onPress={() => Linking.openURL('https://github.com/ClimbStack/climbstack-app')} > Linking.openURL('https://github.com/gizano/LevelUp/issues')} + onPress={() => Linking.openURL('https://github.com/ClimbStack/climbstack-app/issues')} > { + jest.setTimeout(15000); + const mockNavigation = { navigate: jest.fn(), setOptions: jest.fn(), @@ -92,7 +94,7 @@ describe('HomeScreen', () => { beforeEach(() => { (AsyncStorage as any).__INTERNAL_MOCK_STORAGE__ = { - '@levelup/onboarding_done': 'true', + '@climbstack/onboarding_done': 'true', }; jest.clearAllMocks(); jest.useFakeTimers(); @@ -132,7 +134,7 @@ describe('HomeScreen', () => { await render(); expect(mockNavigation.setOptions).toHaveBeenCalledWith({ - title: 'LevelUp', + title: 'ClimbStack', headerStyle: { backgroundColor: Colors.light.surface }, headerTintColor: Colors.light.text, }); @@ -147,7 +149,7 @@ describe('HomeScreen', () => { await render(); expect(mockNavigation.setOptions).toHaveBeenCalledWith({ - title: 'LevelUp', + title: 'ClimbStack', headerStyle: { backgroundColor: Colors.dark.surface }, headerTintColor: Colors.dark.text, }); @@ -156,7 +158,7 @@ describe('HomeScreen', () => { describe('onboarding modal lifecycle', () => { it('shows onboarding modal when onboarding is not marked as done', async () => { - await AsyncStorage.removeItem('@levelup/onboarding_done'); + await AsyncStorage.removeItem('@climbstack/onboarding_done'); const { findByText } = await render( @@ -166,7 +168,7 @@ describe('HomeScreen', () => { }); it('dismisses onboarding modal and stores onboarding_done flag', async () => { - await AsyncStorage.removeItem('@levelup/onboarding_done'); + await AsyncStorage.removeItem('@climbstack/onboarding_done'); const { findByText, queryByText } = await render( @@ -175,7 +177,7 @@ describe('HomeScreen', () => { const skipButton = await findByText(t('onboarding.skip')); await fireEvent.press(skipButton); - const doneFlag = await AsyncStorage.getItem('@levelup/onboarding_done'); + const doneFlag = await AsyncStorage.getItem('@climbstack/onboarding_done'); expect(doneFlag).toBe('true'); expect(queryByText(t('onboarding.skip'))).toBeNull(); }); @@ -192,7 +194,7 @@ describe('HomeScreen', () => { }); it('handles AsyncStorage error during onboarding dismissal gracefully', async () => { - await AsyncStorage.removeItem('@levelup/onboarding_done'); + await AsyncStorage.removeItem('@climbstack/onboarding_done'); const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(AsyncStorage, 'setItem').mockRejectedValueOnce(new Error('Save failure')); @@ -502,10 +504,10 @@ describe('HomeScreen', () => { ], }; - await AsyncStorage.setItem('@levelup/week/2026-10-05', JSON.stringify(weekPlan)); + await AsyncStorage.setItem('@climbstack/week/2026-10-05', JSON.stringify(weekPlan)); jest .spyOn(AsyncStorage, 'getAllKeys') - .mockResolvedValueOnce(['@levelup/week/2026-10-05', '@levelup/week/missing-value']); + .mockResolvedValueOnce(['@climbstack/week/2026-10-05', '@climbstack/week/missing-value']); mockUsePeaks.mockReturnValue({ peaks: [ diff --git a/src/store/.gitkeep b/src/store/.gitkeep index f968e57..582c1cc 100644 --- a/src/store/.gitkeep +++ b/src/store/.gitkeep @@ -1 +1 @@ -// LevelUp state management and persistence +// ClimbStack state management and persistence diff --git a/src/store/__tests__/plannerStorage.test.ts b/src/store/__tests__/plannerStorage.test.ts index b7ba6d3..7ea11f2 100644 --- a/src/store/__tests__/plannerStorage.test.ts +++ b/src/store/__tests__/plannerStorage.test.ts @@ -97,7 +97,7 @@ describe('plannerStorage', () => { await saveWeeklyPlan(plan); - expect(await AsyncStorage.getItem('@levelup/week/2026-10-05')).toBe(JSON.stringify(plan)); + expect(await AsyncStorage.getItem('@climbstack/week/2026-10-05')).toBe(JSON.stringify(plan)); }); test('round-trips a plan', async () => { @@ -292,14 +292,14 @@ describe('plannerStorage', () => { const categories = [makeCategory()]; await saveCategories(categories); await saveTemplates(templates); - await AsyncStorage.setItem('@levelup/streak', '3'); + await AsyncStorage.setItem('@climbstack/streak', '3'); const hours = await scrubDeletedTemplates(['tpl-1'], templates); expect(hours).toBe(0); expect(await loadCategories()).toEqual(categories); expect(await loadTemplates()).toEqual(templates); - expect(await AsyncStorage.getItem('@levelup/streak')).toBe('3'); + expect(await AsyncStorage.getItem('@climbstack/streak')).toBe('3'); }); test('leaves unrelated plans byte-for-byte untouched', async () => { @@ -309,7 +309,7 @@ describe('plannerStorage', () => { await scrubDeletedTemplates(['tpl-1'], templates); - expect(await AsyncStorage.getItem('@levelup/week/2026-10-05')).toBe( + expect(await AsyncStorage.getItem('@climbstack/week/2026-10-05')).toBe( JSON.stringify(unrelated) ); }); diff --git a/src/store/__tests__/storage.test.ts b/src/store/__tests__/storage.test.ts index 418a62c..760105a 100644 --- a/src/store/__tests__/storage.test.ts +++ b/src/store/__tests__/storage.test.ts @@ -85,9 +85,9 @@ describe('storage', () => { test('stores values as strings under the expected keys', async () => { await saveStreak(4, '2026-10-02', 1.25); - expect(await AsyncStorage.getItem('@levelup/streak')).toBe('4'); - expect(await AsyncStorage.getItem('@levelup/last_active')).toBe('2026-10-02'); - expect(await AsyncStorage.getItem('@levelup/completed_hours')).toBe('1.25'); + expect(await AsyncStorage.getItem('@climbstack/streak')).toBe('4'); + expect(await AsyncStorage.getItem('@climbstack/last_active')).toBe('2026-10-02'); + expect(await AsyncStorage.getItem('@climbstack/completed_hours')).toBe('1.25'); }); test('overwrites a previous streak', async () => { @@ -98,7 +98,7 @@ describe('storage', () => { }); test('treats a partially stored streak as defaults for missing fields', async () => { - await AsyncStorage.setItem('@levelup/streak', '5'); + await AsyncStorage.setItem('@climbstack/streak', '5'); expect(await loadStreak()).toEqual({ streak: 5, @@ -107,10 +107,22 @@ describe('storage', () => { }); }); + test('reads from legacy @levelup keys when @climbstack keys are absent', async () => { + await AsyncStorage.setItem('@levelup/streak', '7'); + await AsyncStorage.setItem('@levelup/last_active', '2026-10-01'); + await AsyncStorage.setItem('@levelup/completed_hours', '3.5'); + + expect(await loadStreak()).toEqual({ + streak: 7, + lastActiveDate: '2026-10-01', + totalCompletedHours: 3.5, + }); + }); + test('parses streak as integer and hours as float from raw strings', async () => { await AsyncStorage.multiSet([ - ['@levelup/streak', '6.9'], - ['@levelup/completed_hours', '2.75'], + ['@climbstack/streak', '6.9'], + ['@climbstack/completed_hours', '2.75'], ]); const loaded = await loadStreak(); diff --git a/src/store/plannerStorage.ts b/src/store/plannerStorage.ts index d2f7019..3c050d7 100644 --- a/src/store/plannerStorage.ts +++ b/src/store/plannerStorage.ts @@ -3,6 +3,12 @@ import type { Category, BlockTemplate, WeeklyPlan } from '../types'; import { deleteCalendarEvent } from '../utils/calendar'; const KEYS = { + CATEGORIES: '@climbstack/categories', + TEMPLATES: '@climbstack/templates', + WEEKLY_PLAN_PREFIX: '@climbstack/week/', +} as const; + +const LEGACY_KEYS = { CATEGORIES: '@levelup/categories', TEMPLATES: '@levelup/templates', WEEKLY_PLAN_PREFIX: '@levelup/week/', @@ -15,7 +21,10 @@ export async function saveCategories(categories: Category[]): Promise { } export async function loadCategories(): Promise { - const raw = await AsyncStorage.getItem(KEYS.CATEGORIES); + let raw = await AsyncStorage.getItem(KEYS.CATEGORIES); + if (!raw) { + raw = await AsyncStorage.getItem(LEGACY_KEYS.CATEGORIES); + } if (!raw) return []; return JSON.parse(raw) as Category[]; } @@ -27,7 +36,10 @@ export async function saveTemplates(templates: BlockTemplate[]): Promise { } export async function loadTemplates(): Promise { - const raw = await AsyncStorage.getItem(KEYS.TEMPLATES); + let raw = await AsyncStorage.getItem(KEYS.TEMPLATES); + if (!raw) { + raw = await AsyncStorage.getItem(LEGACY_KEYS.TEMPLATES); + } if (!raw) return []; return JSON.parse(raw) as BlockTemplate[]; } @@ -41,7 +53,10 @@ export async function saveWeeklyPlan(plan: WeeklyPlan): Promise { export async function loadWeeklyPlan(weekId: string): Promise { const key = KEYS.WEEKLY_PLAN_PREFIX + weekId; - const raw = await AsyncStorage.getItem(key); + let raw = await AsyncStorage.getItem(key); + if (!raw) { + raw = await AsyncStorage.getItem(LEGACY_KEYS.WEEKLY_PLAN_PREFIX + weekId); + } if (!raw) return null; return JSON.parse(raw) as WeeklyPlan; } @@ -54,7 +69,9 @@ export async function scrubDeletedTemplates( let totalHoursSubtracted = 0; const allKeys = await AsyncStorage.getAllKeys(); - const planKeys = allKeys.filter((k) => k.startsWith(KEYS.WEEKLY_PLAN_PREFIX)); + const planKeys = allKeys.filter( + (k) => k.startsWith(KEYS.WEEKLY_PLAN_PREFIX) || k.startsWith(LEGACY_KEYS.WEEKLY_PLAN_PREFIX) + ); for (const key of planKeys) { const raw = await AsyncStorage.getItem(key); diff --git a/src/store/storage.ts b/src/store/storage.ts index 7e0f3d7..8260b68 100644 --- a/src/store/storage.ts +++ b/src/store/storage.ts @@ -2,6 +2,13 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import type { Peak } from '../types'; const KEYS = { + PEAKS: '@climbstack/peaks', + STREAK: '@climbstack/streak', + LAST_ACTIVE: '@climbstack/last_active', + COMPLETED_HOURS: '@climbstack/completed_hours', +} as const; + +const LEGACY_KEYS = { PEAKS: '@levelup/peaks', STREAK: '@levelup/streak', LAST_ACTIVE: '@levelup/last_active', @@ -15,7 +22,10 @@ export async function savePeaks(peaks: Peak[]): Promise { /** Carica tutte le vette */ export async function loadPeaks(): Promise { - const raw = await AsyncStorage.getItem(KEYS.PEAKS); + let raw = await AsyncStorage.getItem(KEYS.PEAKS); + if (!raw) { + raw = await AsyncStorage.getItem(LEGACY_KEYS.PEAKS); + } if (!raw) return []; return JSON.parse(raw) as Peak[]; } @@ -39,11 +49,14 @@ export async function loadStreak(): Promise<{ lastActiveDate?: string; totalCompletedHours: number; }> { - const results = await AsyncStorage.multiGet([ - KEYS.STREAK, - KEYS.LAST_ACTIVE, - KEYS.COMPLETED_HOURS, - ]); + let results = await AsyncStorage.multiGet([KEYS.STREAK, KEYS.LAST_ACTIVE, KEYS.COMPLETED_HOURS]); + if (!results[0][1] && !results[1][1] && !results[2][1]) { + results = await AsyncStorage.multiGet([ + LEGACY_KEYS.STREAK, + LEGACY_KEYS.LAST_ACTIVE, + LEGACY_KEYS.COMPLETED_HOURS, + ]); + } const streakStr = results[0][1]; const lastActive = results[1][1]; const hoursStr = results[2][1]; diff --git a/src/types/.gitkeep b/src/types/.gitkeep index 872b966..90e4b1d 100644 --- a/src/types/.gitkeep +++ b/src/types/.gitkeep @@ -1 +1 @@ -// LevelUp TypeScript type definitions +// ClimbStack TypeScript type definitions diff --git a/src/types/index.ts b/src/types/index.ts index 244eba4..4393b1f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { t } from '../utils/i18n'; /** - * LevelUp — Core type definitions + * ClimbStack — Core type definitions * * Modello dati basato sulla metafora alpinistica: * - Peak (Vetta): obiettivo a lungo termine diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep index b62dea9..9acca17 100644 --- a/src/utils/.gitkeep +++ b/src/utils/.gitkeep @@ -1 +1 @@ -// LevelUp utility functions +// ClimbStack utility functions diff --git a/src/utils/__tests__/calendar.test.ts b/src/utils/__tests__/calendar.test.ts index 69c25d3..51b539b 100644 --- a/src/utils/__tests__/calendar.test.ts +++ b/src/utils/__tests__/calendar.test.ts @@ -56,7 +56,7 @@ describe('calendar utils', () => { setPlatform('android'); grantPermission(); calendarStub.getCalendarsAsync.mockResolvedValue([ - makeCalendar({ id: 'cal-existing', title: 'LevelUp - Study' }), + makeCalendar({ id: 'cal-existing', title: 'ClimbStack - Study' }), ]); calendarStub.createEventAsync.mockResolvedValue('evt-123'); }); @@ -198,12 +198,12 @@ describe('calendar utils', () => { await createCalendarEvent('X', 'lun', '09:00', 1, 'Sport', '#00ff00', '2026-10-05'); expect(calendarStub.createCalendarAsync).toHaveBeenCalledWith({ - title: 'LevelUp - Sport', + title: 'ClimbStack - Sport', color: '#00ff00', entityType: 'event', sourceId: 'src-main', source: primary.source, - name: 'LevelUp - Sport', + name: 'ClimbStack - Sport', ownerAccount: 'main@example.com', accessLevel: 'owner', }); diff --git a/src/utils/calendar.ts b/src/utils/calendar.ts index 989038c..1f20dec 100644 --- a/src/utils/calendar.ts +++ b/src/utils/calendar.ts @@ -15,8 +15,9 @@ async function getOrCreateCategoryCalendar( if (status !== 'granted') return null; const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); - const calName = `LevelUp - ${catName}`; - const existing = calendars.find((c) => c.title === calName); + const calName = `ClimbStack - ${catName}`; + const legacyCalName = `LevelUp - ${catName}`; + const existing = calendars.find((c) => c.title === calName || c.title === legacyCalName); if (existing) { // If the color changed, we could update it, but keep it simple for now return existing.id; diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index e04a4f4..51f50d6 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -215,9 +215,9 @@ const translations = { exportSuccessMsg: 'Backup exported successfully!', exportErrorTitle: 'Error', exportErrorMsg: 'Failed to export data.', - aboutTitle: 'About LevelUp', + aboutTitle: 'About ClimbStack', aboutDesc1: - 'LevelUp was born from a simple need: eliminating decision fatigue in free time. The mountain climbing metaphor helps tracking long-term goals (Peaks) and breaking them down into actionable steps (Camps).', + 'ClimbStack was born from a simple need: eliminating decision fatigue in free time. The mountain climbing metaphor helps tracking long-term goals (Peaks) and breaking them down into actionable steps (Camps).', aboutDesc2: 'This app is 100% Open Source and built by developers, for everyone.', starGithub: 'Star on GitHub', submitIssue: 'Submit an Issue', @@ -435,9 +435,9 @@ const translations = { exportSuccessMsg: 'Backup esportato con successo!', exportErrorTitle: 'Errore', exportErrorMsg: "Errore durante l'esportazione del backup.", - aboutTitle: 'About LevelUp', + aboutTitle: 'About ClimbStack', aboutDesc1: - 'LevelUp nasce da una necessità semplice: eliminare la "decision fatigue" nel tempo libero. La metafora dell\'arrampicata aiuta a tracciare obiettivi a lungo termine (Vette) e suddividerli in passaggi concreti (Campi).', + 'ClimbStack nasce da una necessità semplice: eliminare la "decision fatigue" nel tempo libero. La metafora dell\'arrampicata aiuta a tracciare obiettivi a lungo termine (Vette) e suddividerli in passaggi concreti (Campi).', aboutDesc2: 'Questa app è 100% Open Source, creata da sviluppatori per tutti.', starGithub: 'Metti una stella su GitHub', submitIssue: 'Aggiungi una Issue', @@ -653,9 +653,9 @@ const translations = { exportSuccessMsg: 'Backup erfolgreich exportiert!', exportErrorTitle: 'Fehler', exportErrorMsg: 'Fehler beim Exportieren der Daten.', - aboutTitle: 'Über LevelUp', + aboutTitle: 'Über ClimbStack', aboutDesc1: - 'LevelUp entstand aus einem einfachen Bedürfnis: Entscheidungsmüdigkeit in der Freizeit zu beseitigen. Die Bergsteiger-Metapher hilft dabei, langfristige Ziele (Gipfel) zu verfolgen und in konkrete Schritte (Camps) zu unterteilen.', + 'ClimbStack entstand aus einem einfachen Bedürfnis: Entscheidungsmüdigkeit in der Freizeit zu beseitigen. Die Bergsteiger-Metapher hilft dabei, langfristige Ziele (Gipfel) zu verfolgen und in konkrete Schritte (Camps) zu unterteilen.', aboutDesc2: 'Diese App ist zu 100 % Open Source und wurde von Entwicklern für alle geschaffen.', starGithub: 'Mit Stern auf GitHub markieren', @@ -873,9 +873,9 @@ const translations = { exportSuccessMsg: 'Sauvegarde exportée avec succès !', exportErrorTitle: 'Erreur', exportErrorMsg: "Échec de l'exportation des données.", - aboutTitle: 'À propos de LevelUp', + aboutTitle: 'À propos de ClimbStack', aboutDesc1: - "LevelUp est né d'un besoin simple : éliminer la fatigue décisionnelle pendant le temps libre. La métaphore de l'escalade permet de suivre des objectifs à long terme (Sommets) et de les décomposer en étapes concrètes (Camps).", + "ClimbStack est né d'un besoin simple : éliminer la fatigue décisionnelle pendant le temps libre. La métaphore de l'escalade permet de suivre des objectifs à long terme (Sommets) et de les décomposer en étapes concrètes (Camps).", aboutDesc2: 'Cette application est 100 % Open Source et conçue par des développeurs pour tous.', starGithub: 'Mettre une étoile sur GitHub', @@ -1093,9 +1093,9 @@ const translations = { exportSuccessMsg: '¡Copia exportada con éxito!', exportErrorTitle: 'Error', exportErrorMsg: 'Error al exportar los datos.', - aboutTitle: 'Acerca de LevelUp', + aboutTitle: 'Acerca de ClimbStack', aboutDesc1: - 'LevelUp nació de una necesidad sencilla: eliminar la fatiga de decisión en el tiempo libre. La metáfora de la escalada ayuda a seguir objetivos a largo plazo (Cimas) y dividirlos en pasos concretos (Campamentos).', + 'ClimbStack nació de una necesidad sencilla: eliminar la fatiga de decisión en el tiempo libre. La metáfora de la escalada ayuda a seguir objetivos a largo plazo (Cimas) y dividirlos en pasos concretos (Campamentos).', aboutDesc2: 'Esta aplicación es 100% de código abierto y creada por desarrolladores para todos.', starGithub: 'Dar una estrella en GitHub', diff --git a/src/utils/theme.ts b/src/utils/theme.ts index d826c0c..92a8130 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -1,5 +1,5 @@ /** - * LevelUp — Theme constants + * ClimbStack — Theme constants * * Palette "Alpine Dusk": * - Warm stone and charcoal for backgrounds