diff --git a/convex/auth.ts b/convex/auth.ts index 10dc097..62abd30 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -1,6 +1,7 @@ import { ConvexError, v } from 'convex/values'; import { convexAuth, getAuthUserId } from '@convex-dev/auth/server'; import { mutation, query } from './_generated/server'; +import { type Id } from './_generated/dataModel'; import Password from './CustomPassword'; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ @@ -12,19 +13,23 @@ export const getUser = query(async (ctx) => { if (!userId) return null; const user = await ctx.db.get(userId); if (!user) throw new ConvexError('User not found.'); + const image: Id<'_storage'> | null = + typeof user.image === 'string' && user.image.length > 0 + ? user.image as Id<'_storage'> + : null return { id: user._id, email: user.email ?? null, name: user.name ?? null, - image: user.image ?? null, + image, }; }); export const updateUserImage = mutation({ args: { - storageId: v.id('_storage') + storageId: v.id('_storage'), }, - handler: async (ctx, {storageId}) => { + handler: async (ctx, { storageId }) => { const userId = await getAuthUserId(ctx); if (!userId) throw new ConvexError('Not authenticated.'); await ctx.db.patch(userId, { image: storageId }); diff --git a/convex/files.ts b/convex/files.ts index bb76e28..f5a41fc 100644 --- a/convex/files.ts +++ b/convex/files.ts @@ -1,12 +1,12 @@ -import { mutation, query } from "./_generated/server"; -import { v } from "convex/values"; +import { mutation, query } from './_generated/server'; +import { v } from 'convex/values'; export const generateUploadUrl = mutation(async (ctx) => { return await ctx.storage.generateUploadUrl(); }); export const getImageUrl = query({ - args: { storageId: v.id("_storage") }, + args: { storageId: v.id('_storage') }, handler: async (ctx, { storageId }) => { return await ctx.storage.getUrl(storageId); }, diff --git a/src/app/(auth)/profile/page.tsx b/src/app/(auth)/profile/page.tsx index 647fb43..4173039 100644 --- a/src/app/(auth)/profile/page.tsx +++ b/src/app/(auth)/profile/page.tsx @@ -1,8 +1,8 @@ 'use server'; -import { preloadQuery } from "convex/nextjs"; -import { api } from "~/convex/_generated/api"; -import { AvatarUpload, ProfileHeader } from "@/components/layout/profile"; -import { Card } from "@/components/ui"; +import { preloadQuery } from 'convex/nextjs'; +import { api } from '~/convex/_generated/api'; +import { AvatarUpload, ProfileHeader } from '@/components/layout/profile'; +import { Card } from '@/components/ui'; const Profile = async () => { const preloadedUser = await preloadQuery(api.auth.getUser); diff --git a/src/components/layout/header/controls/AvatarDropdown.tsx b/src/components/layout/header/controls/AvatarDropdown.tsx index 6de88ac..593469a 100644 --- a/src/components/layout/header/controls/AvatarDropdown.tsx +++ b/src/components/layout/header/controls/AvatarDropdown.tsx @@ -1,5 +1,4 @@ 'use client'; - import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { @@ -20,8 +19,12 @@ export const AvatarDropdown = () => { const router = useRouter(); const { isLoading, isAuthenticated } = useConvexAuth(); const { signOut } = useAuthActions(); - const user = useQuery(api.auth.getUser); const { tvMode, toggleTVMode } = useTVMode(); + const user = useQuery(api.auth.getUser); + const currentImageUrl = useQuery( + api.files.getImageUrl, + user?.image ? { storageId: user.image } : 'skip', + ); if (isLoading) return ; if (!isAuthenticated) return
; @@ -29,7 +32,7 @@ export const AvatarDropdown = () => { ; -} +}; + +type UploadResponse = { + storageId: Id<'_storage'>; +}; const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => { const user = usePreloadedQuery(preloadedUser); + const [isUploading, setIsUploading] = useState(false); + + const generateUploadUrl = useMutation(api.files.generateUploadUrl); + const updateUserImage = useMutation(api.auth.updateUserImage); + + // Get the current image URL from the storage ID + const currentImageUrl = useQuery( + api.files.getImageUrl, + user?.image ? { storageId: user.image } : 'skip', + ); + + const handleFileUpload = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + alert('Please select an image file'); + return; + } + + setIsUploading(true); + + try { + // Step 1: Get upload URL + const postUrl = await generateUploadUrl(); + + // Step 2: Upload file + const result = await fetch(postUrl, { + method: 'POST', + headers: { 'Content-Type': file.type }, + body: file, + }); + + const uploadResponse = await result.json() as UploadResponse; + + // Step 3: Update user's image field with storage ID + await updateUserImage({ storageId: uploadResponse.storageId }); + } catch (error) { + console.error('Upload failed:', error); + alert('Upload failed. Please try again.'); + } finally { + setIsUploading(false); + } + }; + return (
{}} + onClick={() => document.getElementById('avatar-upload')?.click()} > { />
- { - // - //{isUploading && ( - //
- // - //Uploading... - //
- //)} - } + + + + {isUploading && ( +
+ + Uploading... +
+ )}
); diff --git a/src/components/layout/profile/header.tsx b/src/components/layout/profile/header.tsx index e464645..34dbfcf 100644 --- a/src/components/layout/profile/header.tsx +++ b/src/components/layout/profile/header.tsx @@ -1,15 +1,11 @@ 'use client'; import { type Preloaded, usePreloadedQuery } from 'convex/react'; import { type api } from '~/convex/_generated/api'; -import { - CardHeader, - CardTitle, - CardDescription, -} from '@/components/ui'; +import { CardHeader, CardTitle, CardDescription } from '@/components/ui'; type ProfileCardProps = { preloadedUser: Preloaded; -} +}; const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => { const user = usePreloadedQuery(preloadedUser);