diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 6bc2bee..6f58851 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -15,6 +15,7 @@ import type { } from "convex/server"; import type * as CustomPassword from "../CustomPassword.js"; import type * as auth from "../auth.js"; +import type * as files from "../files.js"; import type * as http from "../http.js"; import type * as myFunctions from "../myFunctions.js"; @@ -29,6 +30,7 @@ import type * as myFunctions from "../myFunctions.js"; declare const fullApi: ApiFromModules<{ CustomPassword: typeof CustomPassword; auth: typeof auth; + files: typeof files; http: typeof http; myFunctions: typeof myFunctions; }>; diff --git a/convex/auth.ts b/convex/auth.ts index 5e91884..10dc097 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -1,5 +1,6 @@ +import { ConvexError, v } from 'convex/values'; import { convexAuth, getAuthUserId } from '@convex-dev/auth/server'; -import { query } from './_generated/server'; +import { mutation, query } from './_generated/server'; import Password from './CustomPassword'; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ @@ -10,7 +11,7 @@ export const getUser = query(async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return null; const user = await ctx.db.get(userId); - if (!user) return null; + if (!user) throw new ConvexError('User not found.'); return { id: user._id, email: user.email ?? null, @@ -18,3 +19,15 @@ export const getUser = query(async (ctx) => { image: user.image ?? null, }; }); + +export const updateUserImage = mutation({ + args: { + storageId: v.id('_storage') + }, + handler: async (ctx, {storageId}) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new ConvexError('Not authenticated.'); + await ctx.db.patch(userId, { image: storageId }); + return { success: true }; + }, +}); diff --git a/convex/files.ts b/convex/files.ts new file mode 100644 index 0000000..bb76e28 --- /dev/null +++ b/convex/files.ts @@ -0,0 +1,13 @@ +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") }, + 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 958621c..647fb43 100644 --- a/src/app/(auth)/profile/page.tsx +++ b/src/app/(auth)/profile/page.tsx @@ -1,4 +1,16 @@ -const Profile = () => { - return
; +'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"; + +const Profile = async () => { + const preloadedUser = await preloadQuery(api.auth.getUser); + return ( + + + + + ); }; export default Profile; diff --git a/src/components/layout/profile/avatar-upload.tsx b/src/components/layout/profile/avatar-upload.tsx new file mode 100644 index 0000000..38cec08 --- /dev/null +++ b/src/components/layout/profile/avatar-upload.tsx @@ -0,0 +1,69 @@ +'use client'; +import { type Preloaded, usePreloadedQuery } from 'convex/react'; +import { type api } from '~/convex/_generated/api'; +import { + BasedAvatar, + CardContent, +} from '@/components/ui'; +import { Loader2, Pencil, Upload } from 'lucide-react'; + +type AvatarUploadProps = { + preloadedUser: Preloaded; +} + +const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => { + const user = usePreloadedQuery(preloadedUser); + return ( + +
+
{}} + > + +
+ +
+
+ +
+
+ { + // + //{isUploading && ( + //
+ // + //Uploading... + //
+ //)} + } +
+
+ ); +}; + +export { AvatarUpload }; diff --git a/src/components/layout/profile/header.tsx b/src/components/layout/profile/header.tsx new file mode 100644 index 0000000..e464645 --- /dev/null +++ b/src/components/layout/profile/header.tsx @@ -0,0 +1,28 @@ +'use client'; +import { type Preloaded, usePreloadedQuery } from 'convex/react'; +import { type api } from '~/convex/_generated/api'; +import { + CardHeader, + CardTitle, + CardDescription, +} from '@/components/ui'; + +type ProfileCardProps = { + preloadedUser: Preloaded; +} + +const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => { + const user = usePreloadedQuery(preloadedUser); + return ( + + + {user?.name ?? user?.email ?? 'Your Profile'} + + + Manage your personal information & how it appears to others. + + + ); +}; + +export { ProfileHeader }; diff --git a/src/components/layout/profile/index.tsx b/src/components/layout/profile/index.tsx new file mode 100644 index 0000000..90f3137 --- /dev/null +++ b/src/components/layout/profile/index.tsx @@ -0,0 +1,2 @@ +export { AvatarUpload } from './avatar-upload'; +export { ProfileHeader } from './header'; diff --git a/src/middleware.ts b/src/middleware.ts index 6e9b6c4..fce771e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -6,7 +6,7 @@ import { import { banSuspiciousIPs } from '@/lib/middleware/ban-suspicious-ips'; const isSignInPage = createRouteMatcher(['/signin']); -const isProtectedRoute = createRouteMatcher(['/', '/server']); +const isProtectedRoute = createRouteMatcher(['/', '/profile']); export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => { const banResponse = banSuspiciousIPs(request);