diff --git a/src/components/privacy/BlockedUsers.tsx b/src/components/privacy/BlockedUsers.tsx new file mode 100644 index 00000000..37b8da5b --- /dev/null +++ b/src/components/privacy/BlockedUsers.tsx @@ -0,0 +1,192 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { UserX, Trash2 } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '@/hooks/useAuth'; +import { useToast } from '@/hooks/use-toast'; + +interface BlockedUser { + id: string; + blocked_id: string; + reason?: string; + created_at: string; + blocked_profile?: { + username: string; + display_name?: string; + avatar_url?: string; + }; +} + +export function BlockedUsers() { + const { user } = useAuth(); + const { toast } = useToast(); + const [blockedUsers, setBlockedUsers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (user) { + fetchBlockedUsers(); + } + }, [user]); + + const fetchBlockedUsers = async () => { + if (!user) return; + + try { + // First get the blocked user IDs + const { data: blocks, error: blocksError } = await supabase + .from('user_blocks') + .select('id, blocked_id, reason, created_at') + .eq('blocker_id', user.id) + .order('created_at', { ascending: false }); + + if (blocksError) throw blocksError; + + if (!blocks || blocks.length === 0) { + setBlockedUsers([]); + return; + } + + // Then get the profile information for blocked users + const blockedIds = blocks.map(b => b.blocked_id); + const { data: profiles, error: profilesError } = await supabase + .from('profiles') + .select('user_id, username, display_name, avatar_url') + .in('user_id', blockedIds); + + if (profilesError) throw profilesError; + + // Combine the data + const blockedUsersWithProfiles = blocks.map(block => ({ + ...block, + blocked_profile: profiles?.find(p => p.user_id === block.blocked_id) + })); + + setBlockedUsers(blockedUsersWithProfiles); + } catch (error: any) { + console.error('Error fetching blocked users:', error); + toast({ + title: 'Error', + description: 'Failed to load blocked users', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + const handleUnblock = async (blockId: string, username: string) => { + try { + const { error } = await supabase + .from('user_blocks') + .delete() + .eq('id', blockId); + + if (error) throw error; + + setBlockedUsers(prev => prev.filter(block => block.id !== blockId)); + toast({ + title: 'User unblocked', + description: `You have unblocked @${username}` + }); + } catch (error: any) { + toast({ + title: 'Error', + description: 'Failed to unblock user', + variant: 'destructive' + }); + } + }; + + if (loading) { + return ( +
+ {Array.from({ length: 2 }, (_, i) => ( +
+
+
+
+
+
+
+
+ ))} +
+ ); + } + + if (blockedUsers.length === 0) { + return ( +
+ +

No blocked users

+

+ Blocked users will appear here and can be unblocked at any time. +

+
+ ); + } + + return ( +
+ {blockedUsers.map((block) => ( +
+ + + + {(block.blocked_profile?.display_name || block.blocked_profile?.username || 'U').charAt(0).toUpperCase()} + + + +
+
+ {block.blocked_profile?.display_name || block.blocked_profile?.username || 'Unknown User'} +
+ {block.blocked_profile?.display_name && ( +
+ @{block.blocked_profile.username} +
+ )} + {block.reason && ( +
+ Reason: {block.reason} +
+ )} +
+ Blocked {new Date(block.created_at).toLocaleDateString()} +
+
+ + + + + + + + Unblock User + + Are you sure you want to unblock @{block.blocked_profile?.username}? + They will be able to interact with your content again. + + + + Cancel + handleUnblock(block.id, block.blocked_profile?.username || 'user')} + > + Unblock + + + + +
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/profile/LocationDisplay.tsx b/src/components/profile/LocationDisplay.tsx new file mode 100644 index 00000000..ba4b2f87 --- /dev/null +++ b/src/components/profile/LocationDisplay.tsx @@ -0,0 +1,55 @@ +import { useState, useEffect } from 'react'; +import { MapPin } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; + +interface LocationDisplayProps { + location: { + city?: string; + country: string; + }; + userId: string; + isOwnProfile: boolean; +} + +export function LocationDisplay({ location, userId, isOwnProfile }: LocationDisplayProps) { + const [showLocation, setShowLocation] = useState(false); + + useEffect(() => { + fetchLocationPrivacy(); + }, [userId, isOwnProfile]); + + const fetchLocationPrivacy = async () => { + // Always show location for own profile + if (isOwnProfile) { + setShowLocation(true); + return; + } + + try { + const { data } = await supabase + .from('user_preferences') + .select('privacy_settings') + .eq('user_id', userId) + .maybeSingle(); + + if (data?.privacy_settings) { + const settings = data.privacy_settings as any; + setShowLocation(settings.show_location || false); + } + } catch (error) { + console.error('Error fetching location privacy:', error); + setShowLocation(false); + } + }; + + if (!showLocation) { + return null; + } + + return ( +
+ + {location.city ? `${location.city}, ${location.country}` : location.country} +
+ ); +} \ No newline at end of file diff --git a/src/components/profile/UserBlockButton.tsx b/src/components/profile/UserBlockButton.tsx new file mode 100644 index 00000000..5c78b21c --- /dev/null +++ b/src/components/profile/UserBlockButton.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { UserX } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '@/hooks/useAuth'; +import { useToast } from '@/hooks/use-toast'; + +interface UserBlockButtonProps { + targetUserId: string; + targetUsername: string; + variant?: 'default' | 'outline' | 'ghost'; + size?: 'default' | 'sm' | 'lg'; +} + +export function UserBlockButton({ targetUserId, targetUsername, variant = 'outline', size = 'sm' }: UserBlockButtonProps) { + const { user } = useAuth(); + const { toast } = useToast(); + const [reason, setReason] = useState(''); + const [loading, setLoading] = useState(false); + + const handleBlock = async () => { + if (!user) return; + + setLoading(true); + try { + const { error } = await supabase + .from('user_blocks') + .insert({ + blocker_id: user.id, + blocked_id: targetUserId, + reason: reason.trim() || null + }); + + if (error) throw error; + + toast({ + title: 'User blocked', + description: `You have blocked @${targetUsername}. They will no longer be able to interact with your content.` + }); + + setReason(''); + } catch (error: any) { + console.error('Error blocking user:', error); + toast({ + title: 'Error', + description: 'Failed to block user', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + // Don't show block button if user is not logged in or trying to block themselves + if (!user || user.id === targetUserId) { + return null; + } + + return ( + + + + + + + Block @{targetUsername} + + This user will no longer be able to see your content or interact with you. + You can unblock them at any time from your privacy settings. + + + +
+ + setReason(e.target.value)} + maxLength={200} + /> +
+ + + Cancel + + {loading ? 'Blocking...' : 'Block User'} + + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/settings/PrivacyTab.tsx b/src/components/settings/PrivacyTab.tsx index 164aa3cb..b2aa4664 100644 --- a/src/components/settings/PrivacyTab.tsx +++ b/src/components/settings/PrivacyTab.tsx @@ -10,6 +10,7 @@ import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; import { supabase } from '@/integrations/supabase/client'; import { Eye, UserX, Shield, Search } from 'lucide-react'; +import { BlockedUsers } from '@/components/privacy/BlockedUsers'; interface PrivacySettings { activity_visibility: 'public' | 'private'; search_visibility: boolean; @@ -284,13 +285,7 @@ export function PrivacyTab() { -
- -

No blocked users

-

- Blocked users will appear here and can be unblocked at any time. -

-
+
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 21212e84..2b142385 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -27,13 +27,16 @@ import { ArrowLeft, Check, AlertCircle, - Loader2 + Loader2, + UserX } from 'lucide-react'; import { Profile as ProfileType } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { PhotoUpload } from '@/components/upload/PhotoUpload'; import { profileEditSchema } from '@/lib/validation'; +import { LocationDisplay } from '@/components/profile/LocationDisplay'; +import { UserBlockButton } from '@/components/profile/UserBlockButton'; export default function Profile() { const { username } = useParams<{ username?: string }>(); @@ -346,17 +349,25 @@ export default function Profile() { className="mb-4" /> - {isOwnProfile && !editing && ( - - )} +
+ {isOwnProfile && !editing && ( + + )} + + {!isOwnProfile && ( + + )} +
@@ -480,12 +491,22 @@ export default function Profile() { })}
- {profile.location && ( + {/* Show pronouns if enabled */} + {profile.show_pronouns && profile.preferred_pronouns && (
- - {profile.location.city}, {profile.location.country} + + {profile.preferred_pronouns}
)} + + {/* Show location only if privacy allows */} + {profile.location && ( + + )} )}