Got the profile picture thing working!
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { ConvexError, v } from 'convex/values';
|
import { ConvexError, v } from 'convex/values';
|
||||||
import { convexAuth, getAuthUserId } from '@convex-dev/auth/server';
|
import { convexAuth, getAuthUserId } from '@convex-dev/auth/server';
|
||||||
import { mutation, query } from './_generated/server';
|
import { mutation, query } from './_generated/server';
|
||||||
|
import { type Id } from './_generated/dataModel';
|
||||||
import Password from './CustomPassword';
|
import Password from './CustomPassword';
|
||||||
|
|
||||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||||
@@ -12,19 +13,23 @@ export const getUser = query(async (ctx) => {
|
|||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
const user = await ctx.db.get(userId);
|
const user = await ctx.db.get(userId);
|
||||||
if (!user) throw new ConvexError('User not found.');
|
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 {
|
return {
|
||||||
id: user._id,
|
id: user._id,
|
||||||
email: user.email ?? null,
|
email: user.email ?? null,
|
||||||
name: user.name ?? null,
|
name: user.name ?? null,
|
||||||
image: user.image ?? null,
|
image,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateUserImage = mutation({
|
export const updateUserImage = mutation({
|
||||||
args: {
|
args: {
|
||||||
storageId: v.id('_storage')
|
storageId: v.id('_storage'),
|
||||||
},
|
},
|
||||||
handler: async (ctx, {storageId}) => {
|
handler: async (ctx, { storageId }) => {
|
||||||
const userId = await getAuthUserId(ctx);
|
const userId = await getAuthUserId(ctx);
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
if (!userId) throw new ConvexError('Not authenticated.');
|
||||||
await ctx.db.patch(userId, { image: storageId });
|
await ctx.db.patch(userId, { image: storageId });
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from './_generated/server';
|
||||||
import { v } from "convex/values";
|
import { v } from 'convex/values';
|
||||||
|
|
||||||
export const generateUploadUrl = mutation(async (ctx) => {
|
export const generateUploadUrl = mutation(async (ctx) => {
|
||||||
return await ctx.storage.generateUploadUrl();
|
return await ctx.storage.generateUploadUrl();
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getImageUrl = query({
|
export const getImageUrl = query({
|
||||||
args: { storageId: v.id("_storage") },
|
args: { storageId: v.id('_storage') },
|
||||||
handler: async (ctx, { storageId }) => {
|
handler: async (ctx, { storageId }) => {
|
||||||
return await ctx.storage.getUrl(storageId);
|
return await ctx.storage.getUrl(storageId);
|
||||||
},
|
},
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { preloadQuery } from "convex/nextjs";
|
import { preloadQuery } from 'convex/nextjs';
|
||||||
import { api } from "~/convex/_generated/api";
|
import { api } from '~/convex/_generated/api';
|
||||||
import { AvatarUpload, ProfileHeader } from "@/components/layout/profile";
|
import { AvatarUpload, ProfileHeader } from '@/components/layout/profile';
|
||||||
import { Card } from "@/components/ui";
|
import { Card } from '@/components/ui';
|
||||||
|
|
||||||
const Profile = async () => {
|
const Profile = async () => {
|
||||||
const preloadedUser = await preloadQuery(api.auth.getUser);
|
const preloadedUser = await preloadQuery(api.auth.getUser);
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
@@ -20,8 +19,12 @@ export const AvatarDropdown = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isLoading, isAuthenticated } = useConvexAuth();
|
const { isLoading, isAuthenticated } = useConvexAuth();
|
||||||
const { signOut } = useAuthActions();
|
const { signOut } = useAuthActions();
|
||||||
const user = useQuery(api.auth.getUser);
|
|
||||||
const { tvMode, toggleTVMode } = useTVMode();
|
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 <BasedAvatar className='animate-pulse' />;
|
if (isLoading) return <BasedAvatar className='animate-pulse' />;
|
||||||
if (!isAuthenticated) return <div />;
|
if (!isAuthenticated) return <div />;
|
||||||
@@ -29,7 +32,7 @@ export const AvatarDropdown = () => {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={user?.image}
|
src={currentImageUrl}
|
||||||
fullName={user?.name}
|
fullName={user?.name}
|
||||||
className='lg:h-10 lg:w-10 my-auto'
|
className='lg:h-10 lg:w-10 my-auto'
|
||||||
fallbackProps={{ className: 'text-xl font-semibold' }}
|
fallbackProps={{ className: 'text-xl font-semibold' }}
|
||||||
|
@@ -1,27 +1,82 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { type Preloaded, usePreloadedQuery } from 'convex/react';
|
import { useState } from 'react';
|
||||||
import { type api } from '~/convex/_generated/api';
|
|
||||||
import {
|
import {
|
||||||
BasedAvatar,
|
type Preloaded,
|
||||||
CardContent,
|
usePreloadedQuery,
|
||||||
} from '@/components/ui';
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from 'convex/react';
|
||||||
|
import { api } from '~/convex/_generated/api';
|
||||||
|
import { BasedAvatar, CardContent } from '@/components/ui';
|
||||||
import { Loader2, Pencil, Upload } from 'lucide-react';
|
import { Loader2, Pencil, Upload } from 'lucide-react';
|
||||||
|
import { type Id } from '~/convex/_generated/dataModel';
|
||||||
|
|
||||||
type AvatarUploadProps = {
|
type AvatarUploadProps = {
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type UploadResponse = {
|
||||||
|
storageId: Id<'_storage'>;
|
||||||
|
};
|
||||||
|
|
||||||
const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||||
const user = usePreloadedQuery(preloadedUser);
|
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<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
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 (
|
return (
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className='flex flex-col items-center'>
|
<div className='flex flex-col items-center'>
|
||||||
<div
|
<div
|
||||||
className='relative group cursor-pointer mb-4'
|
className='relative group cursor-pointer mb-4'
|
||||||
onClick={() => {}}
|
onClick={() => document.getElementById('avatar-upload')?.click()}
|
||||||
>
|
>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={user?.image}
|
src={currentImageUrl} // This will be the generated URL
|
||||||
fullName={user?.name}
|
fullName={user?.name}
|
||||||
className='h-32 w-32'
|
className='h-32 w-32'
|
||||||
fallbackProps={{ className: 'text-4xl font-semibold' }}
|
fallbackProps={{ className: 'text-4xl font-semibold' }}
|
||||||
@@ -45,22 +100,22 @@ const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
|
||||||
//<input
|
<input
|
||||||
//ref={fileInputRef}
|
id='avatar-upload'
|
||||||
//type='file'
|
type='file'
|
||||||
//accept='image/*'
|
accept='image/*'
|
||||||
//className='hidden'
|
className='hidden'
|
||||||
//onChange={handleFileChange}
|
onChange={handleFileUpload}
|
||||||
//disabled={isUploading}
|
disabled={isUploading}
|
||||||
///>
|
/>
|
||||||
//{isUploading && (
|
|
||||||
//<div className='flex items-center text-sm text-gray-500 mt-2'>
|
{isUploading && (
|
||||||
//<Loader2 className='h-4 w-4 mr-2 animate-spin' />
|
<div className='flex items-center text-sm text-gray-500 mt-2'>
|
||||||
//Uploading...
|
<Loader2 className='h-4 w-4 mr-2 animate-spin' />
|
||||||
//</div>
|
Uploading...
|
||||||
//)}
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
);
|
);
|
||||||
|
@@ -1,15 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { type Preloaded, usePreloadedQuery } from 'convex/react';
|
import { type Preloaded, usePreloadedQuery } from 'convex/react';
|
||||||
import { type api } from '~/convex/_generated/api';
|
import { type api } from '~/convex/_generated/api';
|
||||||
import {
|
import { CardHeader, CardTitle, CardDescription } from '@/components/ui';
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
} from '@/components/ui';
|
|
||||||
|
|
||||||
type ProfileCardProps = {
|
type ProfileCardProps = {
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => {
|
const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => {
|
||||||
const user = usePreloadedQuery(preloadedUser);
|
const user = usePreloadedQuery(preloadedUser);
|
||||||
|
Reference in New Issue
Block a user