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';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
|
||||||
interface PhotoUploadProps {
|
interface PhotoUploadProps {
|
||||||
onUploadComplete?: (urls: string[]) => void;
|
onUploadComplete?: (urls: string[], imageId?: string) => void;
|
||||||
onUploadStart?: () => void;
|
onUploadStart?: () => void;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
maxFiles?: number;
|
maxFiles?: number;
|
||||||
@@ -24,6 +24,7 @@ interface PhotoUploadProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
variant?: 'default' | 'compact' | 'avatar';
|
variant?: 'default' | 'compact' | 'avatar';
|
||||||
accept?: string;
|
accept?: string;
|
||||||
|
currentImageId?: string; // For cleanup of existing image
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadedImage {
|
interface UploadedImage {
|
||||||
@@ -41,7 +42,8 @@ export function PhotoUpload({
|
|||||||
existingPhotos = [],
|
existingPhotos = [],
|
||||||
className,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
accept = 'image/jpeg,image/png,image/webp'
|
accept = 'image/jpeg,image/png,image/webp',
|
||||||
|
currentImageId
|
||||||
}: PhotoUploadProps) {
|
}: PhotoUploadProps) {
|
||||||
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@@ -164,6 +166,19 @@ export function PhotoUpload({
|
|||||||
onUploadStart?.();
|
onUploadStart?.();
|
||||||
|
|
||||||
try {
|
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) => {
|
const uploadPromises = filesToUpload.map(async (file, index) => {
|
||||||
setUploadProgress((index / filesToUpload.length) * 100);
|
setUploadProgress((index / filesToUpload.length) * 100);
|
||||||
return uploadFile(file);
|
return uploadFile(file);
|
||||||
@@ -174,7 +189,7 @@ export function PhotoUpload({
|
|||||||
if (isAvatar) {
|
if (isAvatar) {
|
||||||
// For avatars, replace all existing images
|
// For avatars, replace all existing images
|
||||||
setUploadedImages(results);
|
setUploadedImages(results);
|
||||||
onUploadComplete?.(results.map(img => img.url));
|
onUploadComplete?.(results.map(img => img.url), results[0]?.id);
|
||||||
} else {
|
} else {
|
||||||
// For regular uploads, append to existing images
|
// For regular uploads, append to existing images
|
||||||
setUploadedImages(prev => [...prev, ...results]);
|
setUploadedImages(prev => [...prev, ...results]);
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
profiles: {
|
profiles: {
|
||||||
Row: {
|
Row: {
|
||||||
|
avatar_image_id: string | null
|
||||||
avatar_url: string | null
|
avatar_url: string | null
|
||||||
bio: string | null
|
bio: string | null
|
||||||
coaster_count: number | null
|
coaster_count: number | null
|
||||||
@@ -305,6 +306,7 @@ export type Database = {
|
|||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
avatar_image_id?: string | null
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null
|
||||||
bio?: string | null
|
bio?: string | null
|
||||||
coaster_count?: number | null
|
coaster_count?: number | null
|
||||||
@@ -324,6 +326,7 @@ export type Database = {
|
|||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
avatar_image_id?: string | null
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null
|
||||||
bio?: string | null
|
bio?: string | null
|
||||||
coaster_count?: number | 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 { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
MapPin,
|
MapPin,
|
||||||
@@ -32,6 +33,7 @@ export default function Profile() {
|
|||||||
const { username } = useParams<{ username?: string }>();
|
const { username } = useParams<{ username?: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { refreshProfile } = useAuth();
|
||||||
const [profile, setProfile] = useState<ProfileType | null>(null);
|
const [profile, setProfile] = useState<ProfileType | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
@@ -41,6 +43,7 @@ export default function Profile() {
|
|||||||
bio: '',
|
bio: '',
|
||||||
});
|
});
|
||||||
const [avatarUrl, setAvatarUrl] = useState<string>('');
|
const [avatarUrl, setAvatarUrl] = useState<string>('');
|
||||||
|
const [avatarImageId, setAvatarImageId] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCurrentUser();
|
getCurrentUser();
|
||||||
@@ -73,6 +76,7 @@ export default function Profile() {
|
|||||||
bio: data.bio || '',
|
bio: data.bio || '',
|
||||||
});
|
});
|
||||||
setAvatarUrl(data.avatar_url || '');
|
setAvatarUrl(data.avatar_url || '');
|
||||||
|
setAvatarImageId(data.avatar_image_id || '');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching profile:', error);
|
console.error('Error fetching profile:', error);
|
||||||
@@ -110,6 +114,7 @@ export default function Profile() {
|
|||||||
bio: data.bio || '',
|
bio: data.bio || '',
|
||||||
});
|
});
|
||||||
setAvatarUrl(data.avatar_url || '');
|
setAvatarUrl(data.avatar_url || '');
|
||||||
|
setAvatarImageId(data.avatar_image_id || '');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching profile:', error);
|
console.error('Error fetching profile:', error);
|
||||||
@@ -132,7 +137,8 @@ export default function Profile() {
|
|||||||
.update({
|
.update({
|
||||||
display_name: editForm.display_name,
|
display_name: editForm.display_name,
|
||||||
bio: editForm.bio,
|
bio: editForm.bio,
|
||||||
avatar_url: avatarUrl
|
avatar_url: avatarUrl,
|
||||||
|
avatar_image_id: avatarImageId
|
||||||
})
|
})
|
||||||
.eq('user_id', currentUser.id);
|
.eq('user_id', currentUser.id);
|
||||||
|
|
||||||
@@ -142,7 +148,8 @@ export default function Profile() {
|
|||||||
...prev,
|
...prev,
|
||||||
display_name: editForm.display_name,
|
display_name: editForm.display_name,
|
||||||
bio: editForm.bio,
|
bio: editForm.bio,
|
||||||
avatar_url: avatarUrl
|
avatar_url: avatarUrl,
|
||||||
|
avatar_image_id: avatarImageId
|
||||||
} : null);
|
} : null);
|
||||||
|
|
||||||
setEditing(false);
|
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;
|
const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -212,7 +270,8 @@ export default function Profile() {
|
|||||||
variant="avatar"
|
variant="avatar"
|
||||||
maxFiles={1}
|
maxFiles={1}
|
||||||
existingPhotos={profile.avatar_url ? [profile.avatar_url] : []}
|
existingPhotos={profile.avatar_url ? [profile.avatar_url] : []}
|
||||||
onUploadComplete={(urls) => setAvatarUrl(urls[0] || '')}
|
onUploadComplete={handleAvatarUpload}
|
||||||
|
currentImageId={avatarImageId}
|
||||||
onError={(error) => {
|
onError={(error) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Upload Error",
|
title: "Upload Error",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface Profile {
|
|||||||
display_name?: string;
|
display_name?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
|
avatar_image_id?: string;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
date_of_birth?: string;
|
date_of_birth?: string;
|
||||||
privacy_level: 'public' | 'friends' | 'private';
|
privacy_level: 'public' | 'friends' | 'private';
|
||||||
|
|||||||
@@ -20,6 +20,54 @@ serve(async (req) => {
|
|||||||
throw new Error('Missing Cloudflare credentials')
|
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') {
|
if (req.method === 'POST') {
|
||||||
// Request a direct upload URL from Cloudflare
|
// Request a direct upload URL from Cloudflare
|
||||||
const { metadata = {}, variant = 'public' } = await req.json().catch(() => ({}))
|
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