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>
149 lines
No EOL
4.1 KiB
TypeScript
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]);
|
|
} |