The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
301 lines
6.6 KiB
TypeScript
301 lines
6.6 KiB
TypeScript
import Dexie, { type Table } from 'dexie';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
import type { SkateSpot } from '@/lib/types';
|
|
|
|
// Define the database schema
|
|
interface CachedSpot {
|
|
id: string; // event id
|
|
pubkey: string;
|
|
identifier: string;
|
|
event: NostrEvent;
|
|
spot: SkateSpot;
|
|
cachedAt: number;
|
|
viewedAt: number;
|
|
}
|
|
|
|
interface PendingPost {
|
|
id: string; // local uuid
|
|
kind: number;
|
|
content: string;
|
|
tags: string[][];
|
|
createdAt: number;
|
|
status: 'pending' | 'publishing' | 'failed';
|
|
retries: number;
|
|
}
|
|
|
|
interface OfflineSettings {
|
|
key: string;
|
|
value: string;
|
|
}
|
|
|
|
// Create the Dexie database
|
|
class SkateSpotsCacheDB extends Dexie {
|
|
spots!: Table<CachedSpot, string>;
|
|
pendingPosts!: Table<PendingPost, string>;
|
|
settings!: Table<OfflineSettings, string>;
|
|
|
|
constructor() {
|
|
super('SkateSpotsCacheDB');
|
|
this.version(1).stores({
|
|
spots: 'id, pubkey, identifier, [pubkey+identifier], cachedAt, viewedAt',
|
|
pendingPosts: 'id, kind, status, createdAt',
|
|
settings: 'key',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Singleton database instance
|
|
let db: SkateSpotsCacheDB | null = null;
|
|
|
|
function getDB(): SkateSpotsCacheDB {
|
|
if (!db) {
|
|
db = new SkateSpotsCacheDB();
|
|
}
|
|
return db;
|
|
}
|
|
|
|
// ============ Spot Caching ============
|
|
|
|
/**
|
|
* Cache a spot for offline viewing
|
|
*/
|
|
export async function cacheSpot(event: NostrEvent, spot: SkateSpot): Promise<void> {
|
|
const db = getDB();
|
|
const now = Date.now();
|
|
|
|
await db.spots.put({
|
|
id: event.id,
|
|
pubkey: spot.pubkey,
|
|
identifier: spot.identifier,
|
|
event,
|
|
spot,
|
|
cachedAt: now,
|
|
viewedAt: now,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cache multiple spots at once
|
|
*/
|
|
export async function cacheSpots(spotsWithEvents: Array<{ event: NostrEvent; spot: SkateSpot }>): Promise<void> {
|
|
const db = getDB();
|
|
const now = Date.now();
|
|
|
|
await db.spots.bulkPut(
|
|
spotsWithEvents.map(({ event, spot }) => ({
|
|
id: event.id,
|
|
pubkey: spot.pubkey,
|
|
identifier: spot.identifier,
|
|
event,
|
|
spot,
|
|
cachedAt: now,
|
|
viewedAt: now,
|
|
}))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a cached spot by pubkey and identifier
|
|
*/
|
|
export async function getCachedSpot(pubkey: string, identifier: string): Promise<SkateSpot | null> {
|
|
const db = getDB();
|
|
const cached = await db.spots
|
|
.where('[pubkey+identifier]')
|
|
.equals([pubkey, identifier])
|
|
.first();
|
|
|
|
if (cached) {
|
|
// Update viewedAt
|
|
await db.spots.update(cached.id, { viewedAt: Date.now() });
|
|
return cached.spot;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get all cached spots
|
|
*/
|
|
export async function getAllCachedSpots(): Promise<SkateSpot[]> {
|
|
const db = getDB();
|
|
const cached = await db.spots.toArray();
|
|
return cached.map(c => c.spot);
|
|
}
|
|
|
|
/**
|
|
* Get recently viewed spots (for offline browsing)
|
|
*/
|
|
export async function getRecentlyViewedSpots(limit: number = 50): Promise<SkateSpot[]> {
|
|
const db = getDB();
|
|
const cached = await db.spots
|
|
.orderBy('viewedAt')
|
|
.reverse()
|
|
.limit(limit)
|
|
.toArray();
|
|
return cached.map(c => c.spot);
|
|
}
|
|
|
|
/**
|
|
* Get cache statistics
|
|
*/
|
|
export async function getCacheStats(): Promise<{
|
|
spotCount: number;
|
|
oldestCachedAt: number | null;
|
|
newestCachedAt: number | null;
|
|
totalSizeEstimate: number;
|
|
}> {
|
|
const db = getDB();
|
|
const spots = await db.spots.toArray();
|
|
|
|
if (spots.length === 0) {
|
|
return {
|
|
spotCount: 0,
|
|
oldestCachedAt: null,
|
|
newestCachedAt: null,
|
|
totalSizeEstimate: 0,
|
|
};
|
|
}
|
|
|
|
const cachedTimes = spots.map(s => s.cachedAt);
|
|
const totalSize = spots.reduce((acc, s) => acc + JSON.stringify(s).length, 0);
|
|
|
|
return {
|
|
spotCount: spots.length,
|
|
oldestCachedAt: Math.min(...cachedTimes),
|
|
newestCachedAt: Math.max(...cachedTimes),
|
|
totalSizeEstimate: totalSize,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear old cached spots (older than maxAge in milliseconds)
|
|
*/
|
|
export async function clearOldCache(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
|
|
const db = getDB();
|
|
const cutoff = Date.now() - maxAgeMs;
|
|
|
|
const oldSpots = await db.spots
|
|
.where('cachedAt')
|
|
.below(cutoff)
|
|
.primaryKeys();
|
|
|
|
await db.spots.bulkDelete(oldSpots);
|
|
return oldSpots.length;
|
|
}
|
|
|
|
/**
|
|
* Clear all cached spots
|
|
*/
|
|
export async function clearAllCache(): Promise<void> {
|
|
const db = getDB();
|
|
await db.spots.clear();
|
|
}
|
|
|
|
// ============ Pending Posts Queue ============
|
|
|
|
/**
|
|
* Add a post to the pending queue (for posting when back online)
|
|
*/
|
|
export async function queuePost(kind: number, content: string, tags: string[][]): Promise<string> {
|
|
const db = getDB();
|
|
const id = crypto.randomUUID();
|
|
|
|
await db.pendingPosts.add({
|
|
id,
|
|
kind,
|
|
content,
|
|
tags,
|
|
createdAt: Date.now(),
|
|
status: 'pending',
|
|
retries: 0,
|
|
});
|
|
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Get all pending posts
|
|
*/
|
|
export async function getPendingPosts(): Promise<PendingPost[]> {
|
|
const db = getDB();
|
|
return db.pendingPosts
|
|
.where('status')
|
|
.equals('pending')
|
|
.sortBy('createdAt');
|
|
}
|
|
|
|
/**
|
|
* Update a pending post's status
|
|
*/
|
|
export async function updatePendingPostStatus(
|
|
id: string,
|
|
status: 'pending' | 'publishing' | 'failed',
|
|
retries?: number
|
|
): Promise<void> {
|
|
const db = getDB();
|
|
const update: Partial<PendingPost> = { status };
|
|
if (retries !== undefined) {
|
|
update.retries = retries;
|
|
}
|
|
await db.pendingPosts.update(id, update);
|
|
}
|
|
|
|
/**
|
|
* Remove a pending post (after successful publish)
|
|
*/
|
|
export async function removePendingPost(id: string): Promise<void> {
|
|
const db = getDB();
|
|
await db.pendingPosts.delete(id);
|
|
}
|
|
|
|
/**
|
|
* Get pending posts count
|
|
*/
|
|
export async function getPendingPostsCount(): Promise<number> {
|
|
const db = getDB();
|
|
return db.pendingPosts.where('status').equals('pending').count();
|
|
}
|
|
|
|
// ============ Offline Detection ============
|
|
|
|
/**
|
|
* Check if the app is currently offline
|
|
*/
|
|
export function isOffline(): boolean {
|
|
return !navigator.onLine;
|
|
}
|
|
|
|
/**
|
|
* Listen for online/offline status changes
|
|
*/
|
|
export function onConnectionChange(callback: (isOnline: boolean) => void): () => void {
|
|
const handleOnline = () => callback(true);
|
|
const handleOffline = () => callback(false);
|
|
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
};
|
|
}
|
|
|
|
// ============ Settings ============
|
|
|
|
/**
|
|
* Store a setting in the offline cache
|
|
*/
|
|
export async function setSetting(key: string, value: string): Promise<void> {
|
|
const db = getDB();
|
|
await db.settings.put({ key, value });
|
|
}
|
|
|
|
/**
|
|
* Get a setting from the offline cache
|
|
*/
|
|
export async function getSetting(key: string): Promise<string | null> {
|
|
const db = getDB();
|
|
const setting = await db.settings.get(key);
|
|
return setting?.value ?? null;
|
|
}
|