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
+

+
ClimbStack
Gamify your goals. Conquer your peaks.
@@ -10,12 +10,12 @@
-
+
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!
-
-
+
+
@@ -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