/**
* MyMPSection Component
*
* Main "Find Your MP" section with authentication gating
* Shows user's MP based on postal code or allows manual search
*/
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslations } from 'next-intl';
import { useAuth } from '@/contexts/AuthContext';
import { MapPin, Loader2, AlertCircle, User, MessageSquare, Vote, FileText, DollarSign, Users, Newspaper, Building2, Share2, Bookmark, HelpCircle } from 'lucide-react';
import { Link } from '@/i18n/navigation';
import { useQuery } from '@apollo/client';
import { SEARCH_MPS, GET_MP } from '@/lib/queries';
import { PostalCodeInput } from './PostalCodeInput';
import { UnauthenticatedMPPrompt } from './UnauthenticatedMPPrompt';
import { normalizePostalCode } from '@/lib/postalCodeUtils';
import { getMPPhotoUrl } from '@/lib/utils/mpPhotoUrl';
import { ShareButton } from '@/components/ShareButton';
import { BookmarkButton } from '@/components/bookmarks/BookmarkButton';
interface MPData {
id?: string;
name: string;
riding: string;
party: string;
email?: string;
photo_url?: string;
photo_url_source?: string;
phone?: string;
url?: string;
constituency_office?: string;
offices?: Array<{
type: string;
tel: string;
fax?: string;
postal?: string;
}>;
}
export function MyMPSection() {
const t = useTranslations('mps.myMP');
const { user, profile } = useAuth();
const [postalCode, setPostalCode] = useState('');
const [mpData, setMpData] = useState<MPData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [isExpanded, setIsExpanded] = useState(false);
const [isPendingSave, setIsPendingSave] = useState(false);
// Initialize to null to avoid hydration mismatch, then sync from localStorage
const [preferredMpId, setPreferredMpId] = useState<string | null>(null);
const [isClient, setIsClient] = useState(false);
// Hydration-safe: Load from localStorage only after client-side mount
useEffect(() => {
setIsClient(true);
const stored = localStorage.getItem('preferredMpId');
if (stored && !preferredMpId) {
setPreferredMpId(stored);
}
}, []);
// Sync session value to localStorage and state
useEffect(() => {
if (isClient && profile?.preferred_mp_id && !preferredMpId) {
setPreferredMpId(profile.preferred_mp_id);
localStorage.setItem('preferredMpId', profile.preferred_mp_id);
}
}, [isClient, profile?.preferred_mp_id, preferredMpId]);
// Query to get MP directly by ID if preferred_mp_id exists
const { data: preferredMpData, loading: preferredMpLoading } = useQuery(GET_MP, {
variables: { id: preferredMpId },
skip: !preferredMpId || !!mpData
});
// Query to get the correct database ID for the MP by name
const { data: dbMpData } = useQuery(SEARCH_MPS, {
variables: {
searchTerm: mpData?.name || null,
current: true,
limit: 1
},
skip: !mpData?.name
});
// Function to update user's postal code and preferred MP in database
const updateUserPostalCode = useCallback(async (pc: string, mpId?: string) => {
try {
// Update user profile with postal code and optionally MP ID
const response = await fetch('/api/user/update-postal-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
postal_code: pc,
...(mpId && { preferred_mp_id: mpId })
})
});
if (!response.ok) {
console.error('Failed to update postal code');
}
} catch (err) {
console.error('Error updating postal code:', err);
}
}, []);
// Update MP ID when we get the database response
useEffect(() => {
if (dbMpData?.searchMPs?.[0]?.id) {
const mpId = dbMpData.searchMPs[0].id;
setMpData(prev => {
// Only update if the ID is different
if (prev && prev.id !== mpId) {
return { ...prev, id: mpId };
}
return prev;
});
// Don't auto-save - let user click Save button
}
}, [dbMpData]);
// Load preferred MP from database if available
useEffect(() => {
if (preferredMpData?.mps?.[0] && !mpData) {
const mp = preferredMpData.mps[0];
setMpData({
id: mp.id,
name: mp.name,
riding: mp.riding,
party: mp.party,
email: mp.email,
photo_url: mp.photo_url,
photo_url_source: mp.photo_url_source,
phone: mp.phone,
url: mp.ourcommons_url,
constituency_office: mp.constituency_office
});
// This MP is already saved, so no pending save
setIsPendingSave(false);
}
}, [preferredMpData, mpData]);
// Auto-load MP data if user has postal code in profile (fallback if no preferred_mp_id)
useEffect(() => {
if (profile?.postal_code && !mpData && !preferredMpId && !preferredMpLoading) {
setPostalCode(profile.postal_code);
fetchMPByPostalCode(profile.postal_code);
}
}, [profile?.postal_code, preferredMpId, mpData, preferredMpLoading]);
const fetchMPByPostalCode = async (pc: string) => {
setLoading(true);
setError('');
try {
const normalized = normalizePostalCode(pc);
const response = await fetch(`/api/represent?postalCode=${normalized}`);
if (!response.ok) {
if (response.status === 401) {
throw new Error(t('errors.genericError'));
}
if (response.status === 404) {
throw new Error(t('errors.notFound'));
}
throw new Error(t('errors.genericError'));
}
const data = await response.json();
// Extract House of Commons MP from representatives
const mp = data.representatives?.find(
(rep: any) => rep.representative_set_name === 'House of Commons'
);
if (!mp) {
throw new Error(t('errors.notFound'));
}
setMpData({
id: mp.external_id || mp.url?.split('/').pop(),
name: mp.name,
riding: mp.district_name,
party: mp.party_name,
email: mp.email,
photo_url: mp.photo_url,
phone: mp.phone,
url: mp.url,
offices: mp.offices
});
// Mark as pending save - let user click Save button
setIsPendingSave(true);
} catch (err) {
setError(err instanceof Error ? err.message : t('errors.genericError'));
setMpData(null);
} finally {
setLoading(false);
}
};
const handleSaveMP = async () => {
if (!mpData || !user || !profile) return;
setLoading(true);
try {
// Get the correct database ID if we have it
const mpId = dbMpData?.searchMPs?.[0]?.id || mpData.id;
// Save to localStorage for immediate persistence
if (mpId) {
localStorage.setItem('preferredMpId', mpId);
setPreferredMpId(mpId);
}
// Save to user profile in Supabase
await updateUserPostalCode(postalCode, mpId);
// Clear pending save state
setIsPendingSave(false);
} catch (err) {
setError('Failed to save MP preference');
console.error('Error saving MP:', err);
} finally {
setLoading(false);
}
};
const handleUseLocation = () => {
if (!navigator.geolocation) {
setError(t('errors.geolocationUnavailable'));
return;
}
setLoading(true);
setError('');
navigator.geolocation.getCurrentPosition(
async (position) => {
try {
const { latitude, longitude } = position.coords;
const response = await fetch(`/api/represent?lat=${latitude}&lng=${longitude}`);
if (!response.ok) {
throw new Error(t('errors.genericError'));
}
const data = await response.json();
const mp = data.representatives?.find(
(rep: any) => rep.representative_set_name === 'House of Commons'
);
if (!mp) {
throw new Error(t('errors.notFound'));
}
setMpData({
id: mp.external_id || mp.url?.split('/').pop(),
name: mp.name,
riding: mp.district_name,
party: mp.party_name,
email: mp.email,
photo_url: mp.photo_url,
phone: mp.phone,
url: mp.url,
offices: mp.offices
});
// Mark as pending save - let user click Save button
setIsPendingSave(true);
} catch (err) {
setError(err instanceof Error ? err.message : t('errors.genericError'));
} finally {
setLoading(false);
}
},
(err) => {
setLoading(false);
if (err.code === err.PERMISSION_DENIED) {
setError(t('errors.geolocationDenied'));
} else {
setError(t('errors.geolocationUnavailable'));
}
}
);
};
// If user is not authenticated, show sign-up prompt
if (!user) {
return <UnauthenticatedMPPrompt />;
}
// Get photo URL using the utility with fallbacks
const photoUrl = mpData ? getMPPhotoUrl({
id: mpData.id,
photo_url: mpData.photo_url,
photo_url_source: mpData.photo_url_source
}) : null;
// If MP data is loaded, show the MP card
if (mpData && !isExpanded) {
return (
<div className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-2 border-green-200 dark:border-green-800 rounded-lg p-6">
<div className="flex items-start gap-6">
{/* Left Column: MP Photo + Details */}
<div className="flex items-start gap-4 flex-1">
{/* MP Photo */}
{photoUrl && (
<img
src={photoUrl}
alt={mpData.name}
className="w-24 h-32 rounded-lg object-cover flex-shrink-0 bg-white/50 dark:bg-gray-800/50"
/>
)}
{/* MP Info */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<MapPin className="w-5 h-5 text-green-600 dark:text-green-400" />
<span className="text-xs font-semibold text-green-600 dark:text-green-400 uppercase tracking-wide">
{t('yourMPBadge')}
</span>
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-1">
{mpData.name}
</h3>
<p className="text-gray-700 dark:text-gray-300 mb-3">
{mpData.party} • {mpData.riding}
</p>
{/* Contact Information */}
<div className="space-y-1 text-sm">
{mpData.email && (
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<a
href={`mailto:${mpData.email}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{mpData.email}
</a>
</div>
)}
{mpData.phone && (
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<a
href={`tel:${mpData.phone}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{mpData.phone}
</a>
</div>
)}
</div>
</div>
</div>
{/* Right Column: Constituency Office */}
{mpData.offices && mpData.offices.length > 0 && (
<div className="flex-shrink-0 w-64 px-6 border-l border-gray-200 dark:border-gray-700">
<p className="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-3">Constituency Office</p>
{mpData.offices.map((office, idx) => (
<div key={idx} className="space-y-2">
{office.tel && (
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<a href={`tel:${office.tel}`} className="text-blue-600 dark:text-blue-400 hover:underline text-base">
{office.tel}
</a>
</div>
)}
{office.postal && (
<p className="text-sm text-gray-600 dark:text-gray-400">{office.postal}</p>
)}
</div>
))}
</div>
)}
{/* Top Right: Share, Bookmark, Change buttons */}
<div className="flex items-center gap-2 flex-shrink-0">
{mpData.id && (
<>
<ShareButton
url={`/mps/${mpData.id}`}
title={`${mpData.name} - Member of Parliament`}
description={`${mpData.party} MP for ${mpData.riding}`}
/>
<BookmarkButton
bookmarkData={{
itemId: mpData.id,
itemType: 'mp',
title: mpData.name,
subtitle: `${mpData.party} - ${mpData.riding}`,
url: `/mps/${mpData.id}`,
}}
/>
</>
)}
{isPendingSave ? (
<button
onClick={handleSaveMP}
disabled={loading}
className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-md transition-colors"
>
{loading ? 'Saving...' : 'Save'}
</button>
) : (
<button
onClick={() => setIsExpanded(true)}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-white/50 dark:hover:bg-gray-800/50 rounded-md transition-colors"
>
Change
</button>
)}
</div>
</div>
{/* Constituency Office Address */}
{mpData.constituency_office && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-2">
<Building2 className="w-4 h-4 text-gray-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Constituency Office</p>
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-line">{mpData.constituency_office}</p>
</div>
</div>
</div>
)}
{/* Action Buttons */}
{mpData.id && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex flex-wrap gap-2">
<Link
href={`/mps/${mpData.id}` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-md transition-colors"
>
<User className="w-4 h-4" />
Profile
</Link>
<Link
href={`/mps/${mpData.id}#speeches` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<MessageSquare className="w-4 h-4" />
Speeches
</Link>
<Link
href={`/mps/${mpData.id}#votes` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<Vote className="w-4 h-4" />
Votes
</Link>
<Link
href={`/mps/${mpData.id}#legislation` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<FileText className="w-4 h-4" />
Bills
</Link>
<Link
href={`/mps/${mpData.id}#questions` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<HelpCircle className="w-4 h-4" />
Questions
</Link>
<Link
href={`/mps/${mpData.id}#expenses` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<DollarSign className="w-4 h-4" />
Expenses
</Link>
<Link
href={`/mps/${mpData.id}#committees` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<Users className="w-4 h-4" />
Committees
</Link>
<Link
href={`/mps/${mpData.id}#lobbying` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<Building2 className="w-4 h-4" />
Lobbying
</Link>
<Link
href={`/mps/${mpData.id}#news` as any}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 transition-colors"
>
<Newspaper className="w-4 h-4" />
News
</Link>
</div>
</div>
)}
</div>
);
}
// Show search form
return (
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<div className="flex items-center gap-2 mb-4">
<MapPin className="w-6 h-6 text-blue-600 dark:text-blue-400" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{t('findYourMP')}
</h3>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-2">
{t('enterPostalCode')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-500 mb-4">
{t('postalCodeHint')}
</p>
{/* Postal Code Input */}
<div className="flex gap-2 mb-4">
<div className="flex-1">
<PostalCodeInput
value={postalCode}
onChange={setPostalCode}
onValidPostalCode={fetchMPByPostalCode}
disabled={loading}
error={error}
/>
</div>
<button
onClick={() => fetchMPByPostalCode(postalCode)}
disabled={loading || !postalCode}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded-md transition-colors disabled:cursor-not-allowed"
>
{loading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
'Find'
)}
</button>
</div>
{/* Use Location Button */}
<button
onClick={handleUseLocation}
disabled={loading}
className="w-full flex items-center justify-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<MapPin className="w-4 h-4" />
{t('useMyLocation')}
</button>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-2 text-center">
{t('geolocationNote')}
</p>
{/* Error Message */}
{error && !loading && (
<div className="mt-4 flex items-start gap-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{/* Loading State */}
{loading && (
<div className="mt-4 flex items-center justify-center gap-2 text-gray-600 dark:text-gray-400">
<Loader2 className="w-5 h-5 animate-spin" />
<span>{t('loading')}</span>
</div>
)}
</div>
);
}