Fix privacy settings implementation

This commit is contained in:
gpt-engineer-app[bot]
2025-09-28 20:29:15 +00:00
parent 65e5b8e52f
commit 10bdfa67a2
5 changed files with 388 additions and 22 deletions

View File

@@ -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<BlockedUser[]>([]);
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 (
<div className="space-y-4">
{Array.from({ length: 2 }, (_, i) => (
<div key={i} className="flex items-center gap-3 p-3 rounded-lg border animate-pulse">
<div className="w-10 h-10 bg-muted rounded-full"></div>
<div className="flex-1">
<div className="h-4 bg-muted rounded w-1/3 mb-1"></div>
<div className="h-3 bg-muted rounded w-1/4"></div>
</div>
<div className="w-20 h-8 bg-muted rounded"></div>
</div>
))}
</div>
);
}
if (blockedUsers.length === 0) {
return (
<div className="text-center p-4 text-muted-foreground">
<UserX className="w-8 h-8 mx-auto mb-2" />
<p className="text-sm">No blocked users</p>
<p className="text-xs mt-1">
Blocked users will appear here and can be unblocked at any time.
</p>
</div>
);
}
return (
<div className="space-y-3">
{blockedUsers.map((block) => (
<div key={block.id} className="flex items-center gap-3 p-3 rounded-lg border">
<Avatar className="w-10 h-10">
<AvatarImage src={block.blocked_profile?.avatar_url || ''} />
<AvatarFallback>
{(block.blocked_profile?.display_name || block.blocked_profile?.username || 'U').charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="font-medium">
{block.blocked_profile?.display_name || block.blocked_profile?.username || 'Unknown User'}
</div>
{block.blocked_profile?.display_name && (
<div className="text-sm text-muted-foreground">
@{block.blocked_profile.username}
</div>
)}
{block.reason && (
<div className="text-xs text-muted-foreground mt-1">
Reason: {block.reason}
</div>
)}
<div className="text-xs text-muted-foreground">
Blocked {new Date(block.created_at).toLocaleDateString()}
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm">
<Trash2 className="w-4 h-4 mr-1" />
Unblock
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Unblock User</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to unblock @{block.blocked_profile?.username}?
They will be able to interact with your content again.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleUnblock(block.id, block.blocked_profile?.username || 'user')}
>
Unblock
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
</div>
);
}

View File

@@ -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 (
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{location.city ? `${location.city}, ${location.country}` : location.country}
</div>
);
}

View File

@@ -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 (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={variant} size={size}>
<UserX className="w-4 h-4 mr-2" />
Block User
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Block @{targetUsername}</AlertDialogTitle>
<AlertDialogDescription>
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.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-2">
<Label htmlFor="reason">Reason (optional)</Label>
<Input
id="reason"
placeholder="Why are you blocking this user?"
value={reason}
onChange={(e) => setReason(e.target.value)}
maxLength={200}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleBlock}
disabled={loading}
className="bg-destructive hover:bg-destructive/90"
>
{loading ? 'Blocking...' : 'Block User'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -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() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center p-4 text-muted-foreground">
<UserX className="w-8 h-8 mx-auto mb-2" />
<p className="text-sm">No blocked users</p>
<p className="text-xs mt-1">
Blocked users will appear here and can be unblocked at any time.
</p>
</div>
<BlockedUsers />
</CardContent>
</Card>
</div>

View File

@@ -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 && (
<Button
variant="outline"
size="sm"
onClick={() => setEditing(true)}
className="mt-2"
>
<Edit3 className="w-4 h-4 mr-2" />
Edit Profile
</Button>
)}
<div className="flex flex-col gap-2 mt-2">
{isOwnProfile && !editing && (
<Button
variant="outline"
size="sm"
onClick={() => setEditing(true)}
>
<Edit3 className="w-4 h-4 mr-2" />
Edit Profile
</Button>
)}
{!isOwnProfile && (
<UserBlockButton
targetUserId={profile.user_id}
targetUsername={profile.username}
/>
)}
</div>
</div>
<div className="flex-1">
@@ -480,12 +491,22 @@ export default function Profile() {
})}
</div>
{profile.location && (
{/* Show pronouns if enabled */}
{profile.show_pronouns && profile.preferred_pronouns && (
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{profile.location.city}, {profile.location.country}
<User className="w-4 h-4" />
{profile.preferred_pronouns}
</div>
)}
{/* Show location only if privacy allows */}
{profile.location && (
<LocationDisplay
location={profile.location}
userId={profile.user_id}
isOwnProfile={isOwnProfile}
/>
)}
</div>
</div>
)}