Refactor: Implement avatar cleanup plan

This commit is contained in:
gpt-engineer-app[bot]
2025-09-28 17:38:09 +00:00
parent 728f7c145e
commit 2147514784
6 changed files with 135 additions and 6 deletions

View File

@@ -16,7 +16,7 @@ import { cn } from '@/lib/utils';
import { supabase } from '@/integrations/supabase/client';
interface PhotoUploadProps {
onUploadComplete?: (urls: string[]) => void;
onUploadComplete?: (urls: string[], imageId?: string) => void;
onUploadStart?: () => void;
onError?: (error: string) => void;
maxFiles?: number;
@@ -24,6 +24,7 @@ interface PhotoUploadProps {
className?: string;
variant?: 'default' | 'compact' | 'avatar';
accept?: string;
currentImageId?: string; // For cleanup of existing image
}
interface UploadedImage {
@@ -41,7 +42,8 @@ export function PhotoUpload({
existingPhotos = [],
className,
variant = 'default',
accept = 'image/jpeg,image/png,image/webp'
accept = 'image/jpeg,image/png,image/webp',
currentImageId
}: PhotoUploadProps) {
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
const [uploading, setUploading] = useState(false);
@@ -164,6 +166,19 @@ export function PhotoUpload({
onUploadStart?.();
try {
// Delete old image first if this is an avatar update
if (isAvatar && currentImageId) {
try {
await supabase.functions.invoke('upload-image', {
method: 'DELETE',
body: { imageId: currentImageId }
});
} catch (deleteError) {
console.warn('Failed to delete old avatar:', deleteError);
// Continue with upload even if deletion fails
}
}
const uploadPromises = filesToUpload.map(async (file, index) => {
setUploadProgress((index / filesToUpload.length) * 100);
return uploadFile(file);
@@ -174,7 +189,7 @@ export function PhotoUpload({
if (isAvatar) {
// For avatars, replace all existing images
setUploadedImages(results);
onUploadComplete?.(results.map(img => img.url));
onUploadComplete?.(results.map(img => img.url), results[0]?.id);
} else {
// For regular uploads, append to existing images
setUploadedImages(prev => [...prev, ...results]);

View File

@@ -286,6 +286,7 @@ export type Database = {
}
profiles: {
Row: {
avatar_image_id: string | null
avatar_url: string | null
bio: string | null
coaster_count: number | null
@@ -305,6 +306,7 @@ export type Database = {
username: string
}
Insert: {
avatar_image_id?: string | null
avatar_url?: string | null
bio?: string | null
coaster_count?: number | null
@@ -324,6 +326,7 @@ export type Database = {
username: string
}
Update: {
avatar_image_id?: string | null
avatar_url?: string | null
bio?: string | null
coaster_count?: number | null

View File

@@ -10,6 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useAuth } from '@/hooks/useAuth';
import {
User,
MapPin,
@@ -32,6 +33,7 @@ export default function Profile() {
const { username } = useParams<{ username?: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const { refreshProfile } = useAuth();
const [profile, setProfile] = useState<ProfileType | null>(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
@@ -41,6 +43,7 @@ export default function Profile() {
bio: '',
});
const [avatarUrl, setAvatarUrl] = useState<string>('');
const [avatarImageId, setAvatarImageId] = useState<string>('');
useEffect(() => {
getCurrentUser();
@@ -73,6 +76,7 @@ export default function Profile() {
bio: data.bio || '',
});
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
}
} catch (error: any) {
console.error('Error fetching profile:', error);
@@ -110,6 +114,7 @@ export default function Profile() {
bio: data.bio || '',
});
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
}
} catch (error: any) {
console.error('Error fetching profile:', error);
@@ -132,7 +137,8 @@ export default function Profile() {
.update({
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: avatarUrl
avatar_url: avatarUrl,
avatar_image_id: avatarImageId
})
.eq('user_id', currentUser.id);
@@ -142,7 +148,8 @@ export default function Profile() {
...prev,
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: avatarUrl
avatar_url: avatarUrl,
avatar_image_id: avatarImageId
} : null);
setEditing(false);
@@ -159,6 +166,57 @@ export default function Profile() {
}
};
const handleAvatarUpload = async (urls: string[], imageId?: string) => {
if (!currentUser || !urls[0]) return;
const newAvatarUrl = urls[0];
const newImageId = imageId || '';
// Update local state immediately
setAvatarUrl(newAvatarUrl);
setAvatarImageId(newImageId);
try {
// Update database immediately
const { error } = await supabase
.from('profiles')
.update({
avatar_url: newAvatarUrl,
avatar_image_id: newImageId
})
.eq('user_id', currentUser.id);
if (error) throw error;
// Update local profile state
setProfile(prev => prev ? {
...prev,
avatar_url: newAvatarUrl,
avatar_image_id: newImageId
} : null);
// Refresh the auth context to update header avatar
if (refreshProfile) {
await refreshProfile();
}
toast({
title: "Avatar updated",
description: "Your profile picture has been updated successfully.",
});
} catch (error: any) {
// Revert local state on error
setAvatarUrl(profile?.avatar_url || '');
setAvatarImageId(profile?.avatar_image_id || '');
toast({
variant: "destructive",
title: "Error updating avatar",
description: error.message,
});
}
};
const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id;
if (loading) {
@@ -212,7 +270,8 @@ export default function Profile() {
variant="avatar"
maxFiles={1}
existingPhotos={profile.avatar_url ? [profile.avatar_url] : []}
onUploadComplete={(urls) => setAvatarUrl(urls[0] || '')}
onUploadComplete={handleAvatarUpload}
currentImageId={avatarImageId}
onError={(error) => {
toast({
title: "Upload Error",

View File

@@ -97,6 +97,7 @@ export interface Profile {
display_name?: string;
bio?: string;
avatar_url?: string;
avatar_image_id?: string;
location?: Location;
date_of_birth?: string;
privacy_level: 'public' | 'friends' | 'private';

View File

@@ -20,6 +20,54 @@ serve(async (req) => {
throw new Error('Missing Cloudflare credentials')
}
if (req.method === 'DELETE') {
// Delete image from Cloudflare
const { imageId } = await req.json()
if (!imageId) {
return new Response(
JSON.stringify({ error: 'Image ID is required for deletion' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
const deleteResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`,
},
}
)
const deleteResult = await deleteResponse.json()
if (!deleteResponse.ok) {
console.error('Cloudflare delete error:', deleteResult)
return new Response(
JSON.stringify({
error: 'Failed to delete image',
details: deleteResult.errors || deleteResult.error
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
return new Response(
JSON.stringify({ success: true, deleted: true }),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
if (req.method === 'POST') {
// Request a direct upload URL from Cloudflare
const { metadata = {}, variant = 'public' } = await req.json().catch(() => ({}))

View File

@@ -0,0 +1,3 @@
-- Add avatar_image_id column to profiles table for Cloudflare image management
ALTER TABLE public.profiles
ADD COLUMN avatar_image_id TEXT;