mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-19 05:54:06 +01:00
feat: add edit profile functionality
This commit is contained in:
parent
b0210fb727
commit
c70e327438
134
src/app/(user)/profile/[uid]/edit/page.tsx
Normal file
134
src/app/(user)/profile/[uid]/edit/page.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { updateProfile } from "@/lib/data/profileMutate";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from "@/components/ui/form";
|
||||
import { z } from "zod";
|
||||
import { profileSchema } from "@/types/schemas/profile.schema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import { uploadAvatar } from "@/lib/data/bucket/uploadAvatar";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export default function EditProfilePage({ params }: { params: { uid: string } }) {
|
||||
const uid = params.uid;
|
||||
const client = createSupabaseClient();
|
||||
const router = useRouter();
|
||||
|
||||
const profileForm = useForm<z.infer<typeof profileSchema>>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
});
|
||||
|
||||
const onProfileSubmit = async (updates: z.infer<typeof profileSchema>) => {
|
||||
const { avatars, username, full_name, bio } = updates;
|
||||
|
||||
try {
|
||||
let avatarUrl = null;
|
||||
|
||||
if (avatars instanceof File) {
|
||||
const avatarData = await uploadAvatar(client, avatars, uid);
|
||||
avatarUrl = avatarData?.path
|
||||
? `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${avatarData.path}`
|
||||
: null;
|
||||
}
|
||||
|
||||
const result = await updateProfile(client, uid, {
|
||||
username,
|
||||
full_name,
|
||||
bio,
|
||||
...(avatarUrl && { avatar_url: avatarUrl }),
|
||||
});
|
||||
|
||||
if (result) {
|
||||
toast.success("Profile updated successfully!");
|
||||
router.push(`/profile/${uid}`);
|
||||
} else {
|
||||
toast.error("No fields to update!");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Error updating profile!");
|
||||
console.error("Error updating profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container max-w-screen-xl">
|
||||
<div className="my-5">
|
||||
<span className="text-2xl font-bold">Update Profile</span>
|
||||
<Separator className="my-5" />
|
||||
</div>
|
||||
<div>
|
||||
<Form {...profileForm}>
|
||||
<form onSubmit={profileForm.handleSubmit(onProfileSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={profileForm.control}
|
||||
name="avatars"
|
||||
render={({ field: { value, onChange, ...fieldProps } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Avatar</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...fieldProps}
|
||||
type="file"
|
||||
onChange={(event) => onChange(event.target.files && event.target.files[0])}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={profileForm.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>This is your public display name.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={profileForm.control}
|
||||
name="full_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>This is your public full name.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={profileForm.control}
|
||||
name="bio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Your bio here" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>This is your public bio description.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { getUserProfile } from "@/lib/data/userQuery";
|
||||
import { Tables } from "@/types/database.types";
|
||||
import { format } from "date-fns";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Profile extends Tables<"Profiles"> {}
|
||||
|
||||
@ -33,10 +34,10 @@ export default async function ProfilePage({ params }: { params: { uid: string }
|
||||
return (
|
||||
<div className="container max-w-screen-xl px-4 py-8">
|
||||
<div className="bg-card border-2 border-border shadow-xl rounded-lg overflow-hidden">
|
||||
<div className="bg-cover bg-center h-64 p-4" style={{ backgroundImage: "url(./banner.jpg)" }}>
|
||||
<div className="bg-cover bg-center h-64 p-4" style={{ backgroundImage: "url(/banner.jpg)" }}>
|
||||
<div className="flex justify-end">
|
||||
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Edit Profile
|
||||
<Link href={`/profile/${uid}/edit`}>Edit Profile</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
31
src/lib/data/bucket/uploadAvatar.ts
Normal file
31
src/lib/data/bucket/uploadAvatar.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
export async function uploadAvatar(
|
||||
supabase: SupabaseClient,
|
||||
file: File,
|
||||
uid: string,
|
||||
) {
|
||||
const allowedExtensions = ["jpeg", "jpg", "png"];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
||||
throw new Error(
|
||||
"Invalid file format. Only jpeg, jpg, and png are allowed.",
|
||||
);
|
||||
}
|
||||
|
||||
const fileName = `profile-${uid}.${fileExtension}`;
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from("avatars")
|
||||
.upload(fileName, file, {
|
||||
upsert: true,
|
||||
contentType: `image/${fileExtension}`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
48
src/lib/data/profileMutate.ts
Normal file
48
src/lib/data/profileMutate.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
interface UpdateData {
|
||||
username?: string;
|
||||
full_name?: string;
|
||||
bio?: string;
|
||||
updated_at?: Date;
|
||||
}
|
||||
|
||||
export async function updateProfile(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
updates: UpdateData,
|
||||
) {
|
||||
const updateData: { [key: string]: any | undefined } = {};
|
||||
|
||||
if (updates.username || updates.username != "") {
|
||||
updateData.username = updates.username;
|
||||
}
|
||||
if (updates.full_name || updates.full_name != "") {
|
||||
updateData.full_name = updates.full_name;
|
||||
}
|
||||
if (updates.bio || updates.bio != "") {
|
||||
updateData.bio = updates.bio;
|
||||
}
|
||||
|
||||
updateData.updated_at = new Date();
|
||||
|
||||
if (
|
||||
updateData.username != undefined || updateData.full_name != undefined ||
|
||||
updateData.bio != undefined
|
||||
) {
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update(updateData)
|
||||
.eq("id", userId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating profile:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log("No fields to update.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
27
src/types/schemas/profile.schema.ts
Normal file
27
src/types/schemas/profile.schema.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const MAX_FILE_SIZE = 500000;
|
||||
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png"];
|
||||
|
||||
const profileAvatarSchema = z
|
||||
.custom<File>(
|
||||
(val) =>
|
||||
val && typeof val === "object" && "size" in val && "type" in val,
|
||||
{
|
||||
message: "Input must be a file.",
|
||||
},
|
||||
)
|
||||
.refine((file) => file.size < MAX_FILE_SIZE, {
|
||||
message: "File can't be bigger than 5MB.",
|
||||
})
|
||||
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
|
||||
message: "File format must be either jpg, jpeg, or png.",
|
||||
}).optional();
|
||||
|
||||
export const profileSchema = z.object({
|
||||
username: z.string().min(3).max(50).optional(),
|
||||
full_name: z.string().min(4).max(100).optional(),
|
||||
bio: z.string().min(10).max(1000).optional(),
|
||||
updated_at: z.string().datetime().optional(),
|
||||
avatars: profileAvatarSchema,
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user