SkateSpots/src/components/AppProvider.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
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>
2026-06-15 22:18:28 +07:00

149 lines
No EOL
4.1 KiB
TypeScript

import { ReactNode, useEffect } from 'react';
import { z } from 'zod';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata } from '@/contexts/AppContext';
interface AppProviderProps {
children: ReactNode;
/** Application storage key */
storageKey: string;
/** Default app configuration */
defaultConfig: AppConfig;
}
// Zod schema for RelayMetadata validation
const RelayMetadataSchema = z.object({
relays: z.array(z.object({
url: z.string().url(),
read: z.boolean(),
write: z.boolean(),
})),
updatedAt: z.number(),
}) satisfies z.ZodType<RelayMetadata>;
// Zod schema for AppConfig validation
const AppConfigSchema = z.object({
theme: z.enum(['dark', 'light', 'system']),
relayMetadata: RelayMetadataSchema,
}) satisfies z.ZodType<AppConfig>;
export function AppProvider(props: AppProviderProps) {
const {
children,
storageKey,
defaultConfig,
} = props;
// App configuration state with localStorage persistence
const [rawConfig, setConfig] = useLocalStorage<Partial<AppConfig>>(
storageKey,
{},
{
serialize: JSON.stringify,
deserialize: (value: string) => {
const parsed = JSON.parse(value);
return AppConfigSchema.partial().parse(parsed);
}
}
);
// Generic config updater with callback pattern
const updateConfig = (updater: (currentConfig: Partial<AppConfig>) => Partial<AppConfig>) => {
setConfig(updater);
};
// Merge configs with special handling for relay metadata
const config = { ...defaultConfig, ...rawConfig };
// Force migration to correct relays and dark theme
useEffect(() => {
let needsUpdate = false;
const updates: Partial<AppConfig> = {};
// Check relay configuration
if (config.relayMetadata.relays) {
const currentRelayUrls = config.relayMetadata.relays.map(r => r.url).sort().join(',');
const expectedRelayUrls = defaultConfig.relayMetadata.relays.map(r => r.url).sort().join(',');
if (currentRelayUrls !== expectedRelayUrls) {
console.log('Migrating relays to:', defaultConfig.relayMetadata.relays.map(r => r.url));
updates.relayMetadata = defaultConfig.relayMetadata;
needsUpdate = true;
}
}
// Force dark theme (app is dark-only now)
if (config.theme !== 'dark') {
console.log('Forcing dark theme');
updates.theme = 'dark';
needsUpdate = true;
}
if (needsUpdate) {
updateConfig((current) => ({
...current,
...updates,
}));
}
}, []);
// Ensure relay list is never empty - fallback to default if no relays configured
if (!config.relayMetadata.relays || config.relayMetadata.relays.length === 0) {
config.relayMetadata = defaultConfig.relayMetadata;
}
const appContextValue: AppContextType = {
config,
updateConfig,
};
// Apply theme effects to document
useApplyTheme(config.theme);
return (
<AppContext.Provider value={appContextValue}>
{children}
</AppContext.Provider>
);
}
/**
* Hook to apply theme changes to the document root
*/
function useApplyTheme(theme: Theme) {
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
// Handle system theme changes when theme is set to "system"
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
root.classList.add(systemTheme);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
}