Alpha stage commit
This commit is contained in:
31
apps/mobile/README.md
Normal file
31
apps/mobile/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Mobile App
|
||||
|
||||
Purpose: riding/offline companion.
|
||||
|
||||
MVP mobile scope can be a placeholder until the web planner and backend are stable.
|
||||
|
||||
Primary future screens:
|
||||
|
||||
- Trip list.
|
||||
- Offline pack status.
|
||||
- Riding mode: next water/food/sleep/repair/bailout.
|
||||
- Map view.
|
||||
- Report issue.
|
||||
- Export/share route.
|
||||
|
||||
Offline-critical data:
|
||||
|
||||
- route geometry,
|
||||
- stages,
|
||||
- POIs,
|
||||
- rules,
|
||||
- exports,
|
||||
- report queue.
|
||||
|
||||
## Implemented placeholder
|
||||
|
||||
The current package is not a full React Native shell. It provides compiled TypeScript contracts for the future mobile app:
|
||||
|
||||
- `OfflineReportQueue` for dated, location-bound reports captured without network access;
|
||||
- `OfflinePackIndex` for downloaded route/stage/POI/rule/export manifests;
|
||||
- storage abstraction that can later be backed by SQLite, AsyncStorage, or a native key-value store.
|
||||
15
apps/mobile/package.json
Normal file
15
apps/mobile/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@pikebacker/mobile",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/offlineStore.js",
|
||||
"types": "./dist/offlineStore.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pikebacker/shared": "0.1.0"
|
||||
}
|
||||
}
|
||||
80
apps/mobile/src/offlineStore.ts
Normal file
80
apps/mobile/src/offlineStore.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { LocalReportCreate, OfflinePackManifest } from '@pikebacker/shared';
|
||||
|
||||
export type OfflineReportQueueItem = LocalReportCreate & {
|
||||
offlineClientId: string;
|
||||
queuedAt: string;
|
||||
syncAttempts: number;
|
||||
lastSyncError?: string;
|
||||
};
|
||||
|
||||
export type OfflinePackRecord = {
|
||||
manifest: OfflinePackManifest;
|
||||
downloadedAt: string;
|
||||
routeJsonPath?: string;
|
||||
poiJsonPath?: string;
|
||||
gpxPath?: string;
|
||||
tilePackPath?: string;
|
||||
};
|
||||
|
||||
export interface OfflineKeyValueStore {
|
||||
getItem(key: string): Promise<string | null>;
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
}
|
||||
|
||||
const REPORT_QUEUE_KEY = 'pikebacker.mobile.offlineReportQueue';
|
||||
const PACK_INDEX_KEY = 'pikebacker.mobile.offlinePackIndex';
|
||||
|
||||
export class OfflineReportQueue {
|
||||
constructor(private readonly store: OfflineKeyValueStore) {}
|
||||
|
||||
async enqueue(report: LocalReportCreate, now = new Date().toISOString()): Promise<OfflineReportQueueItem> {
|
||||
const item: OfflineReportQueueItem = {
|
||||
...report,
|
||||
offlineClientId: report.offlineClientId ?? crypto.randomUUID(),
|
||||
queuedAt: now,
|
||||
syncAttempts: 0
|
||||
};
|
||||
const queue = await this.list();
|
||||
await this.store.setItem(REPORT_QUEUE_KEY, JSON.stringify([...queue, item]));
|
||||
return item;
|
||||
}
|
||||
|
||||
async list(): Promise<OfflineReportQueueItem[]> {
|
||||
const raw = await this.store.getItem(REPORT_QUEUE_KEY);
|
||||
return raw ? (JSON.parse(raw) as OfflineReportQueueItem[]) : [];
|
||||
}
|
||||
|
||||
async recordSyncFailure(offlineClientId: string, error: string): Promise<void> {
|
||||
const queue = await this.list();
|
||||
const next = queue.map((item) =>
|
||||
item.offlineClientId === offlineClientId
|
||||
? { ...item, syncAttempts: item.syncAttempts + 1, lastSyncError: error }
|
||||
: item
|
||||
);
|
||||
await this.store.setItem(REPORT_QUEUE_KEY, JSON.stringify(next));
|
||||
}
|
||||
|
||||
async removeSynced(offlineClientId: string): Promise<void> {
|
||||
const queue = await this.list();
|
||||
await this.store.setItem(REPORT_QUEUE_KEY, JSON.stringify(queue.filter((item) => item.offlineClientId !== offlineClientId)));
|
||||
}
|
||||
}
|
||||
|
||||
export class OfflinePackIndex {
|
||||
constructor(private readonly store: OfflineKeyValueStore) {}
|
||||
|
||||
async upsert(record: OfflinePackRecord): Promise<void> {
|
||||
const records = await this.list();
|
||||
const next = [...records.filter((item) => item.manifest.packId !== record.manifest.packId), record];
|
||||
await this.store.setItem(PACK_INDEX_KEY, JSON.stringify(next));
|
||||
}
|
||||
|
||||
async list(): Promise<OfflinePackRecord[]> {
|
||||
const raw = await this.store.getItem(PACK_INDEX_KEY);
|
||||
return raw ? (JSON.parse(raw) as OfflinePackRecord[]) : [];
|
||||
}
|
||||
|
||||
async get(packId: string): Promise<OfflinePackRecord | undefined> {
|
||||
return (await this.list()).find((record) => record.manifest.packId === packId);
|
||||
}
|
||||
}
|
||||
11
apps/mobile/tsconfig.json
Normal file
11
apps/mobile/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user