Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 166 additions & 26 deletions app/(dashboard)/donor/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { useUserStore } from "@/store/useUserStore";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Loader2, Save, Upload, Camera } from "lucide-react";
import { Loader2, Save, Upload, Camera, Edit } from "lucide-react";
import Image from "next/image";
import { account, databases, storage } from "@/lib/appwrite/api";
import { APPWRITE_CONFIG } from "@/lib/appwrite/config";
Expand All @@ -16,11 +16,32 @@ const DonorProfilePage = () => {

// Form State
const [name, setName] = useState("");
const [nameError, setNameError] = useState("");
const [address, setAddress] = useState("");
const [phone, setPhone] = useState("");
const [phoneError, setPhoneError] = useState("");
const [pincode, setPincode] = useState("");
const [profileImage, setProfileImage] = useState<File | null>(null);

// added: editing state and snapshot ref
const [isEditing, setIsEditing] = useState(false);
const originalRef = useRef<{
name: string;
address: string;
phone: string;
pincode: string;
profileImage: File | null;
}>({
name: "",
address: "",
phone: "",
pincode: "",
profileImage: null,
});

// ref for hidden file input used by "Change Photo" button
const fileInputRef = useRef<HTMLInputElement | null>(null);

// Initialize form with user data
useEffect(() => {
if (user) {
Expand All @@ -33,7 +54,7 @@ const DonorProfilePage = () => {

const calculateProgress = () => {
let completed = 0;
// 5 Main fields: Image, Name, Address, Phone, Pincode
// 5 Main fields: Image, Name, Address, Phone, Pincode (State is removed)
if (profileImage || user?.profileImageId) completed++;
if (name.trim()) completed++;
if (address.trim()) completed++;
Expand All @@ -42,8 +63,41 @@ const DonorProfilePage = () => {
return (completed / 5) * 100;
};

const handleSave = async () => {
if (!user) return;
// Validate name: only alphabets and spaces allowed, and required
const validateName = (value: string) => {
const trimmed = value.trim();
if (!trimmed) {
setNameError("Name is required");
return false;
}
const valid = /^[A-Za-z\s]+$/.test(trimmed);
setNameError(valid ? "" : "Name can only contain letters and spaces");
return valid;
};

// Validate phone: exactly 10 digits required
const validatePhone = (value: string) => {
const trimmed = value.trim();
if (!trimmed) {
setPhoneError("Phone is required");
return false;
}
const valid = /^\d{10}$/.test(trimmed);
setPhoneError(valid ? "" : "Phone must be exactly 10 digits");
return valid;
};

const handleSave = async (): Promise<boolean> => {
if (!user) return false;
// final validation before save
if (!validateName(name)) {
alert("Please fix the name field before saving.");
return false;
}
if (!validatePhone(phone)) {
alert("Please fix the phone field before saving.");
return false;
}
setLoading(true);

try {
Expand All @@ -59,16 +113,16 @@ const DonorProfilePage = () => {
);
profileImageId = file.$id;
} catch (error) {
console.error("Image upload failed", error);
// image upload failed (console removed)
}
}

const payload = {
name,
address,
phone: parseInt(phone.replace(/\D/g, '') || '0'),
pincode: parseInt(pincode.replace(/\D/g, '') || '0'),
profileImageId
phone: parseInt(phone.replace(/\D/g, "") || "0"),
pincode: parseInt(pincode.replace(/\D/g, "") || "0"),
profileImageId,
};

// 2. Update Profile Document
Expand Down Expand Up @@ -98,22 +152,55 @@ const DonorProfilePage = () => {
try {
await account.updateName(name);
} catch (e) {
console.error("Failed to update account name", e);
// failed to update account name (console removed)
}
}

// 4. Update Local State
await checkUser();
// Optional: Add toast notification here

return true;
} catch (error) {
console.error("Profile update failed:", error);
// Profile update failed (console removed)
alert("Failed to save profile. Please ensure information is correct.");
return false;
} finally {
setLoading(false);
}
};

// start editing: snapshot current values
const startEditing = () => {
originalRef.current = {
name,
address,
phone,
pincode,
profileImage,
};
// validate current snapshot fields
validateName(name);
validatePhone(phone);
setIsEditing(true);
};

// cancel editing: restore snapshot and disable editing
const cancelEditing = () => {
setName(originalRef.current.name);
setNameError("");
setPhoneError("");
setAddress(originalRef.current.address);
setPhone(originalRef.current.phone);
setPincode(originalRef.current.pincode);
setProfileImage(originalRef.current.profileImage);
setIsEditing(false);
};

const triggerFilePicker = () => {
fileInputRef.current?.click();
};

if (!user) return <div className="p-8 text-center text-neutral-500">Loading profile...</div>;

const currentAvatarUrl = user.profileImageId
Expand All @@ -129,10 +216,34 @@ const DonorProfilePage = () => {
<h1 className="text-3xl font-bold text-neutral-800 dark:text-white">Donor Profile</h1>
<p className="text-neutral-500 mt-1">Manage your personal information and preferences.</p>
</div>
<Button onClick={handleSave} disabled={loading} className="bg-emerald-500 hover:bg-emerald-600 text-white min-w-[140px]">
{loading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}
Save Changes
</Button>

{/* Header buttons: Edit / Save & Cancel */}
{!isEditing ? (
<Button
onClick={startEditing}
className="min-w-[140px] flex items-center gap-2 border border-emerald-200 text-emerald-600 bg-white hover:bg-emerald-50"
>
<Edit className="w-4 h-4" />
Edit Profile
</Button>
) : (
<div className="flex gap-3">
<Button
onClick={async () => {
const success = await handleSave();
if (success) setIsEditing(false);
}}
disabled={loading || !!nameError || !!phoneError}
className="bg-emerald-500 hover:bg-emerald-600 text-white min-w-[140px]"
>
{loading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}
Save Changes
</Button>
<Button onClick={cancelEditing} disabled={loading} className="bg-white text-neutral-700 border border-neutral-200 hover:bg-neutral-50 min-w-[120px]">
Cancel
</Button>
</div>
)}
</div>

<div className="space-y-2">
Expand Down Expand Up @@ -164,15 +275,6 @@ const DonorProfilePage = () => {
</div>
)}
</div>
<label className="absolute bottom-0 right-0 p-2 bg-white dark:bg-neutral-800 rounded-full shadow-md cursor-pointer border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors">
<Camera className="w-5 h-5 text-neutral-600 dark:text-neutral-300" />
<input
type="file"
accept="image/*"
className="hidden"
onChange={(e) => setProfileImage(e.target.files?.[0] || null)}
/>
</label>
</div>

<div className="flex-1 space-y-2 text-center sm:text-left pt-2">
Expand All @@ -181,6 +283,27 @@ const DonorProfilePage = () => {
<span className="inline-block px-3 py-1 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400 text-xs font-semibold rounded-full capitalize">
{user.role}
</span>

{/* Change Photo button placed under donor tag (only visible while editing) */}
{isEditing && (
<div className="mt-3">
<Button
onClick={triggerFilePicker}
variant="ghost"
className="inline-flex items-center gap-2 rounded-md bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 px-3 py-1 text-sm"
>
<Upload className="w-4 h-4 text-neutral-600 dark:text-neutral-300" />
Change Photo
</Button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => setProfileImage(e.target.files?.[0] || null)}
/>
</div>
)}
</div>
</div>

Expand All @@ -191,8 +314,16 @@ const DonorProfilePage = () => {
<Input
placeholder="Enter Name"
value={name}
onChange={(e) => setName(e.target.value)}
onChange={(e) => {
// filter out non-alphabet characters (allow spaces)
const filtered = e.target.value.replace(/[^A-Za-z\s]/g, "");
setName(filtered);
// live-validate
validateName(filtered);
}}
disabled={!isEditing}
/>
{nameError ? <p className="text-xs text-red-500 mt-1">{nameError}</p> : null}
</div>

<div className="space-y-2">
Expand All @@ -201,8 +332,15 @@ const DonorProfilePage = () => {
placeholder="Enter Phone Number"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
onChange={(e) => {
// allow digits only and limit to 10 chars
const filtered = e.target.value.replace(/\D/g, "").slice(0, 10);
setPhone(filtered);
validatePhone(filtered);
}}
disabled={!isEditing}
/>
{phoneError ? <p className="text-xs text-red-500 mt-1">{phoneError}</p> : null}
</div>

<div className="space-y-2 md:col-span-2">
Expand All @@ -211,6 +349,7 @@ const DonorProfilePage = () => {
placeholder="House/Flat No, Street, Area"
value={address}
onChange={(e) => setAddress(e.target.value)}
disabled={!isEditing}
/>
</div>

Expand All @@ -221,6 +360,7 @@ const DonorProfilePage = () => {
value={pincode}
onChange={(e) => setPincode(e.target.value)}
maxLength={6}
disabled={!isEditing}
/>
</div>

Expand Down