/**
* NewsSourcesDataContext
* Provides news sources data and user preferences to visualizer components
*/
'use client';
import React, {
createContext,
useContext,
ReactNode,
useState,
useEffect,
useCallback,
useMemo,
} from 'react';
import { useSession } from 'next-auth/react';
export interface NewsSource {
id: string;
name: string;
rss_url: string;
is_active: boolean;
region: string;
website_url: string | null;
description_en: string | null;
description_fr: string | null;
has_paywall: boolean;
credibility_tier: 'premium' | 'standard' | 'aggregator';
logo_url: string | null;
}
export interface NewsPreferences {
id: string;
user_id: string;
disabled_sources: string[];
disabled_regions: string[];
subscribed_sources: string[];
created_at: string;
updated_at: string;
}
interface NewsSourcesDataContextType {
/** All available news sources */
sources: NewsSource[];
/** News sources grouped by region */
sourcesByRegion: Record<string, NewsSource[]>;
/** User's current preferences */
preferences: NewsPreferences | null;
/** Loading state */
loading: boolean;
/** Error state */
error: Error | null;
/** Whether user has PRO subscription (for paywall management) */
isPro: boolean;
// Actions
/** Toggle a source on/off */
toggleSource: (sourceId: string) => void;
/** Toggle a region on/off */
toggleRegion: (regionCode: string) => void;
/** Toggle paywall subscription (PRO only) */
toggleSubscription: (sourceId: string) => void;
/** Save preferences to server */
savePreferences: () => Promise<void>;
/** Whether there are unsaved changes */
hasUnsavedChanges: boolean;
// Computed helpers
/** Check if a source is enabled */
isSourceEnabled: (sourceId: string) => boolean;
/** Check if a region is enabled */
isRegionEnabled: (regionCode: string) => boolean;
/** Check if user has subscription to a source */
hasSubscription: (sourceId: string) => boolean;
/** Get all enabled source IDs */
enabledSources: string[];
/** Get all enabled region codes */
enabledRegions: string[];
/** Get all subscribed source IDs */
subscribedSources: string[];
}
const NewsSourcesDataContext = createContext<NewsSourcesDataContextType | null>(null);
// All Canadian province/territory codes
const ALL_REGIONS = ['national', 'ON', 'QC', 'BC', 'AB', 'MB', 'SK', 'NS', 'NB', 'NL', 'PE', 'NT', 'YT', 'NU'];
export function NewsSourcesDataProvider({ children }: { children: ReactNode }) {
const { data: session, status } = useSession();
const [sources, setSources] = useState<NewsSource[]>([]);
const [sourcesByRegion, setSourcesByRegion] = useState<Record<string, NewsSource[]>>({});
const [preferences, setPreferences] = useState<NewsPreferences | null>(null);
const [savedPreferences, setSavedPreferences] = useState<NewsPreferences | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [isPro, setIsPro] = useState(false);
// Fetch news sources
useEffect(() => {
async function fetchSources() {
try {
const res = await fetch('/api/news-sources');
if (!res.ok) throw new Error('Failed to fetch news sources');
const data = await res.json();
setSources(data.sources || []);
setSourcesByRegion(data.sourcesByRegion || {});
} catch (err) {
console.error('Error fetching news sources:', err);
setError(err instanceof Error ? err : new Error('Unknown error'));
}
}
fetchSources();
}, []);
// Fetch user preferences when authenticated
useEffect(() => {
async function fetchPreferences() {
if (status !== 'authenticated' || !session?.user) {
setLoading(false);
return;
}
try {
const res = await fetch('/api/user/news-preferences');
if (!res.ok) throw new Error('Failed to fetch news preferences');
const data = await res.json();
setPreferences(data.data);
setSavedPreferences(data.data);
// Check PRO status from user profile API
const profileRes = await fetch('/api/user/profile');
if (profileRes.ok) {
const profileData = await profileRes.json();
const tier = (profileData.data?.subscription_tier || 'FREE').toUpperCase();
setIsPro(tier === 'PRO');
}
} catch (err) {
console.error('Error fetching news preferences:', err);
// Don't set error for preferences - use defaults
setPreferences({
id: '',
user_id: session.user.id || '',
disabled_sources: [],
disabled_regions: [],
subscribed_sources: [],
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
} finally {
setLoading(false);
}
}
fetchPreferences();
}, [session, status]);
// Toggle source enabled/disabled
const toggleSource = useCallback((sourceId: string) => {
setPreferences(prev => {
if (!prev) return prev;
const disabled = prev.disabled_sources || [];
const isCurrentlyDisabled = disabled.includes(sourceId);
return {
...prev,
disabled_sources: isCurrentlyDisabled
? disabled.filter(id => id !== sourceId)
: [...disabled, sourceId],
};
});
}, []);
// Toggle region enabled/disabled
const toggleRegion = useCallback((regionCode: string) => {
setPreferences(prev => {
if (!prev) return prev;
const disabled = prev.disabled_regions || [];
const isCurrentlyDisabled = disabled.includes(regionCode);
return {
...prev,
disabled_regions: isCurrentlyDisabled
? disabled.filter(r => r !== regionCode)
: [...disabled, regionCode],
};
});
}, []);
// Toggle subscription to a source (PRO only)
const toggleSubscription = useCallback((sourceId: string) => {
if (!isPro) return; // Only PRO users can manage subscriptions
setPreferences(prev => {
if (!prev) return prev;
const subscribed = prev.subscribed_sources || [];
const isCurrentlySubscribed = subscribed.includes(sourceId);
return {
...prev,
subscribed_sources: isCurrentlySubscribed
? subscribed.filter(id => id !== sourceId)
: [...subscribed, sourceId],
};
});
}, [isPro]);
// Save preferences to server
const savePreferences = useCallback(async () => {
if (!preferences || status !== 'authenticated') return;
try {
const res = await fetch('/api/user/news-preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
disabled_sources: preferences.disabled_sources,
disabled_regions: preferences.disabled_regions,
subscribed_sources: preferences.subscribed_sources,
}),
});
if (!res.ok) throw new Error('Failed to save preferences');
const data = await res.json();
setPreferences(data.data);
setSavedPreferences(data.data);
} catch (err) {
console.error('Error saving preferences:', err);
throw err;
}
}, [preferences, status]);
// Check if there are unsaved changes
const hasUnsavedChanges = useMemo(() => {
if (!preferences || !savedPreferences) return false;
const disabledSourcesChanged =
JSON.stringify([...preferences.disabled_sources].sort()) !==
JSON.stringify([...savedPreferences.disabled_sources].sort());
const disabledRegionsChanged =
JSON.stringify([...preferences.disabled_regions].sort()) !==
JSON.stringify([...savedPreferences.disabled_regions].sort());
const subscribedSourcesChanged =
JSON.stringify([...preferences.subscribed_sources].sort()) !==
JSON.stringify([...savedPreferences.subscribed_sources].sort());
return disabledSourcesChanged || disabledRegionsChanged || subscribedSourcesChanged;
}, [preferences, savedPreferences]);
// Helper functions
const isSourceEnabled = useCallback((sourceId: string): boolean => {
if (!preferences) return true;
return !preferences.disabled_sources.includes(sourceId);
}, [preferences]);
const isRegionEnabled = useCallback((regionCode: string): boolean => {
if (!preferences) return true;
return !preferences.disabled_regions.includes(regionCode);
}, [preferences]);
const hasSubscription = useCallback((sourceId: string): boolean => {
if (!preferences) return false;
return preferences.subscribed_sources.includes(sourceId);
}, [preferences]);
// Computed values
const enabledSources = useMemo(() => {
const disabled = preferences?.disabled_sources || [];
return sources
.filter(s => !disabled.includes(s.id))
.map(s => s.id);
}, [sources, preferences]);
const enabledRegions = useMemo(() => {
const disabled = preferences?.disabled_regions || [];
return ALL_REGIONS.filter(r => !disabled.includes(r));
}, [preferences]);
const subscribedSources = useMemo(() => {
return preferences?.subscribed_sources || [];
}, [preferences]);
const value = useMemo(() => ({
sources,
sourcesByRegion,
preferences,
loading,
error,
isPro,
toggleSource,
toggleRegion,
toggleSubscription,
savePreferences,
hasUnsavedChanges,
isSourceEnabled,
isRegionEnabled,
hasSubscription,
enabledSources,
enabledRegions,
subscribedSources,
}), [
sources,
sourcesByRegion,
preferences,
loading,
error,
isPro,
toggleSource,
toggleRegion,
toggleSubscription,
savePreferences,
hasUnsavedChanges,
isSourceEnabled,
isRegionEnabled,
hasSubscription,
enabledSources,
enabledRegions,
subscribedSources,
]);
return (
<NewsSourcesDataContext.Provider value={value}>
{children}
</NewsSourcesDataContext.Provider>
);
}
export function useNewsSourcesData() {
const context = useContext(NewsSourcesDataContext);
if (!context) {
throw new Error('useNewsSourcesData must be used within a NewsSourcesDataProvider');
}
return context;
}