mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 07:11:12 -05:00
Refactor: Implement avatar cleanup plan
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => ({}))
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user