allows editing profiles

This commit is contained in:
chark1es 2025-01-27 16:46:23 -08:00
parent 903fe32d9b
commit 3684071673
3 changed files with 280 additions and 192 deletions

View file

@ -25,6 +25,14 @@ interface AuthElements {
refreshResumes: HTMLButtonElement; refreshResumes: HTMLButtonElement;
resumeSearch: HTMLInputElement; resumeSearch: HTMLInputElement;
searchResumes: HTMLButtonElement; searchResumes: HTMLButtonElement;
profileEditor: HTMLDialogElement;
editorName: HTMLInputElement;
editorEmail: HTMLInputElement;
editorMemberId: HTMLInputElement;
editorPoints: HTMLInputElement;
editorResume: HTMLInputElement;
editorCurrentResume: HTMLParagraphElement;
saveProfileButton: HTMLButtonElement;
} }
export class StoreAuth { export class StoreAuth {
@ -128,6 +136,31 @@ export class StoreAuth {
"searchResumes", "searchResumes",
) as HTMLButtonElement; ) as HTMLButtonElement;
const profileEditor = document.getElementById(
"profileEditor",
) as HTMLDialogElement;
const editorName = document.getElementById(
"editorName",
) as HTMLInputElement;
const editorEmail = document.getElementById(
"editorEmail",
) as HTMLInputElement;
const editorMemberId = document.getElementById(
"editorMemberId",
) as HTMLInputElement;
const editorPoints = document.getElementById(
"editorPoints",
) as HTMLInputElement;
const editorResume = document.getElementById(
"editorResume",
) as HTMLInputElement;
const editorCurrentResume = document.getElementById(
"editorCurrentResume",
) as HTMLParagraphElement;
const saveProfileButton = document.getElementById(
"saveProfileButton",
) as HTMLButtonElement;
if ( if (
!loginButton || !loginButton ||
!logoutButton || !logoutButton ||
@ -153,7 +186,15 @@ export class StoreAuth {
!resumeList || !resumeList ||
!refreshResumes || !refreshResumes ||
!resumeSearch || !resumeSearch ||
!searchResumes !searchResumes ||
!profileEditor ||
!editorName ||
!editorEmail ||
!editorMemberId ||
!editorPoints ||
!editorResume ||
!editorCurrentResume ||
!saveProfileButton
) { ) {
throw new Error("Required DOM elements not found"); throw new Error("Required DOM elements not found");
} }
@ -184,6 +225,14 @@ export class StoreAuth {
refreshResumes, refreshResumes,
resumeSearch, resumeSearch,
searchResumes, searchResumes,
profileEditor,
editorName,
editorEmail,
editorMemberId,
editorPoints,
editorResume,
editorCurrentResume,
saveProfileButton,
}; };
} }
@ -374,8 +423,13 @@ export class StoreAuth {
} }
private getFileNameFromUrl(url: string): string { private getFileNameFromUrl(url: string): string {
const parts = url.split("/"); try {
return parts[parts.length - 1]; const urlObj = new URL(url);
const pathParts = urlObj.pathname.split("/");
return decodeURIComponent(pathParts[pathParts.length - 1]);
} catch (e) {
return url.split("/").pop() || "Unknown File";
}
} }
private async handleMemberIdButton() { private async handleMemberIdButton() {
@ -436,6 +490,15 @@ export class StoreAuth {
throw new Error("User ID not found"); throw new Error("User ID not found");
} }
// Get current user data first
const currentUser = await this.pb.collection("users").getOne(user.id);
// Keep existing data
formData.append("name", currentUser.name || "");
formData.append("email", currentUser.email || "");
formData.append("member_id", currentUser.member_id || "");
formData.append("points", currentUser.points?.toString() || "0");
await this.pb.collection("users").update(user.id, formData); await this.pb.collection("users").update(user.id, formData);
uploadStatus.textContent = "Resume uploaded successfully!"; uploadStatus.textContent = "Resume uploaded successfully!";
@ -515,7 +578,7 @@ export class StoreAuth {
private async fetchUserResumes(searchQuery: string = "") { private async fetchUserResumes(searchQuery: string = "") {
try { try {
let filter = 'resume != ""'; let filter = ""; // Remove the resume filter to show all users
if (searchQuery) { if (searchQuery) {
const terms = searchQuery const terms = searchQuery
.toLowerCase() .toLowerCase()
@ -528,16 +591,20 @@ export class StoreAuth {
`(name ?~ "${term}" || email ?~ "${term}" || member_id ?~ "${term}")`, `(name ?~ "${term}" || email ?~ "${term}" || member_id ?~ "${term}")`,
) )
.join(" && "); .join(" && ");
filter += ` && (${searchConditions})`; filter = searchConditions; // Only apply search conditions
} }
} }
const records = await this.pb.collection("users").getList(1, 50, { const records = await this.pb.collection("users").getList(1, 50, {
filter, filter,
sort: "-updated", sort: "-updated",
fields: "id,name,email,member_id,resume,updated,points", fields:
"id,name,email,member_id,resume,points,collectionId,collectionName",
expand: "resume",
}); });
console.log("Fetched records:", records.items); // Debug log
const { resumeList } = this.elements; const { resumeList } = this.elements;
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
@ -545,16 +612,19 @@ export class StoreAuth {
const row = document.createElement("tr"); const row = document.createElement("tr");
row.innerHTML = ` row.innerHTML = `
<td colspan="6" class="text-center py-4"> <td colspan="6" class="text-center py-4">
${searchQuery ? "No users found matching your search." : "No resumes uploaded yet."} ${searchQuery ? "No users found matching your search." : "No users found."}
</td> </td>
`; `;
fragment.appendChild(row); fragment.appendChild(row);
} else { } else {
records.items.forEach((user) => { records.items.forEach((user) => {
const row = document.createElement("tr"); const row = document.createElement("tr");
const resumeUrl = user.resume const resumeUrl =
? this.pb.files.getURL(user, user.resume) user.resume && user.resume !== ""
: null; ? this.pb.files.getURL(user, user.resume.toString())
: null;
console.log("User resume:", user.resume, "Resume URL:", resumeUrl); // Debug log
row.innerHTML = ` row.innerHTML = `
<td class="block lg:table-cell"> <td class="block lg:table-cell">
@ -563,40 +633,7 @@ export class StoreAuth {
<div class="font-medium">${user.name || "N/A"}</div> <div class="font-medium">${user.name || "N/A"}</div>
<div class="text-sm opacity-70">${user.email || "N/A"}</div> <div class="text-sm opacity-70">${user.email || "N/A"}</div>
<div class="text-sm opacity-70">ID: ${user.member_id || "N/A"}</div> <div class="text-sm opacity-70">ID: ${user.member_id || "N/A"}</div>
<div class="flex items-center justify-between"> <div class="text-sm opacity-70">Points: ${user.points || 0}</div>
<div class="flex items-center gap-2">
<span class="text-sm opacity-70">Points:</span>
<div class="points-display-${user.id} flex items-center gap-2">
<span class="font-medium">${user.points || 0}</span>
<button class="btn btn-xs btn-ghost edit-points" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
</div>
<div class="points-edit-${user.id} hidden flex items-center gap-2">
<input
type="number"
class="input input-bordered input-xs w-[70px]"
value="${user.points || 0}"
min="0"
data-user-id="${user.id}"
/>
<div class="flex gap-1">
<button class="btn btn-xs btn-ghost confirm-points" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-success" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
<button class="btn btn-xs btn-ghost cancel-points" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-error" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
${ ${
resumeUrl resumeUrl
@ -607,9 +644,12 @@ export class StoreAuth {
` `
: '<span class="text-sm opacity-50">No resume</span>' : '<span class="text-sm opacity-50">No resume</span>'
} }
<span class="text-xs opacity-50"> <button class="btn btn-ghost btn-xs edit-profile" data-user-id="${user.id}">
${new Date(user.updated).toLocaleDateString()} <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
</span> <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
Edit
</button>
</div> </div>
</div> </div>
@ -618,37 +658,7 @@ export class StoreAuth {
</td> </td>
<td class="hidden lg:table-cell">${user.email || "N/A"}</td> <td class="hidden lg:table-cell">${user.email || "N/A"}</td>
<td class="hidden lg:table-cell">${user.member_id || "N/A"}</td> <td class="hidden lg:table-cell">${user.member_id || "N/A"}</td>
<td class="hidden lg:table-cell w-[140px]"> <td class="hidden lg:table-cell">${user.points || 0}</td>
<div class="points-display-${user.id} flex items-center justify-between">
<span class="font-medium min-w-[40px]">${user.points || 0}</span>
<button class="btn btn-xs btn-ghost edit-points" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
</div>
<div class="points-edit-${user.id} hidden flex items-center justify-between">
<input
type="number"
class="input input-bordered input-xs w-[70px]"
value="${user.points || 0}"
min="0"
data-user-id="${user.id}"
/>
<div class="flex gap-1">
<button class="btn btn-xs btn-ghost confirm-points" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-success" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
<button class="btn btn-xs btn-ghost cancel-points" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-error" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</td>
<td class="hidden lg:table-cell"> <td class="hidden lg:table-cell">
${ ${
resumeUrl resumeUrl
@ -660,7 +670,14 @@ export class StoreAuth {
: '<span class="text-sm opacity-50">No resume</span>' : '<span class="text-sm opacity-50">No resume</span>'
} }
</td> </td>
<td class="hidden lg:table-cell">${new Date(user.updated).toLocaleDateString()}</td> <td class="hidden lg:table-cell">
<button class="btn btn-ghost btn-xs edit-profile" data-user-id="${user.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
Edit Profile
</button>
</td>
`; `;
fragment.appendChild(row); fragment.appendChild(row);
@ -670,8 +687,16 @@ export class StoreAuth {
resumeList.innerHTML = ""; resumeList.innerHTML = "";
resumeList.appendChild(fragment); resumeList.appendChild(fragment);
// Setup event listeners for the points editing functionality // Setup edit profile event listeners
this.setupPointsEventListeners(); const editButtons = resumeList.querySelectorAll(".edit-profile");
editButtons.forEach((button) => {
button.addEventListener("click", () => {
const userId = (button as HTMLButtonElement).dataset.userId;
if (userId) {
this.handleProfileEdit(userId);
}
});
});
} catch (err) { } catch (err) {
console.error("Failed to fetch user resumes:", err); console.error("Failed to fetch user resumes:", err);
const { resumeList } = this.elements; const { resumeList } = this.elements;
@ -685,131 +710,102 @@ export class StoreAuth {
} }
} }
private async updateUserPoints(userId: string, points: number) { private async handleProfileEdit(userId: string) {
try { try {
await this.pb.collection("users").update(userId, { const user = await this.pb.collection("users").getOne(userId);
points: points, const {
}); profileEditor,
editorName,
editorEmail,
editorMemberId,
editorPoints,
editorCurrentResume,
saveProfileButton,
} = this.elements;
// Update the display after successful update // Populate the form
const displayElement = document.querySelector( editorName.value = user.name || "";
`.points-display-${userId}`, editorEmail.value = user.email || "";
) as HTMLDivElement; editorMemberId.value = user.member_id || "";
const editElement = document.querySelector( editorPoints.value = user.points?.toString() || "0";
`.points-edit-${userId}`,
) as HTMLDivElement; // Update resume display
if (displayElement && editElement) { if (user.resume) {
const pointsSpan = displayElement.querySelector("span"); const resumeUrl = this.pb.files.getURL(user, user.resume.toString());
if (pointsSpan) { const fileName = this.getFileNameFromUrl(resumeUrl);
pointsSpan.textContent = points.toString(); editorCurrentResume.textContent = `Current resume: ${fileName}`;
} editorCurrentResume.classList.remove("opacity-70");
displayElement.classList.remove("hidden"); } else {
editElement.classList.add("hidden"); editorCurrentResume.textContent = "No resume uploaded";
editorCurrentResume.classList.add("opacity-70");
} }
// Store the user ID for saving
saveProfileButton.dataset.userId = userId;
// Show the dialog
profileEditor.showModal();
} catch (err) { } catch (err) {
console.error("Failed to update points:", err); console.error("Failed to load user for editing:", err);
} }
} }
private setupPointsEventListeners() { private async handleProfileSave() {
// Use event delegation for all points-related actions const {
const { resumeList } = this.elements; profileEditor,
editorName,
editorEmail,
editorMemberId,
editorPoints,
editorResume,
saveProfileButton,
} = this.elements;
const userId = saveProfileButton.dataset.userId;
resumeList.addEventListener("click", async (e) => { if (!userId) {
console.log("Click event triggered"); console.error("No user ID found for saving");
const target = e.target as HTMLElement; return;
const button = target.closest("button"); }
console.log("Button found:", button);
if (!button) return;
const userId = button.dataset.userId; try {
console.log("User ID:", userId); // First get the current user data to check existing resume
if (!userId) return; const currentUser = await this.pb.collection("users").getOne(userId);
// Handle edit button click const formData = new FormData();
if (button.classList.contains("edit-points")) { formData.append("name", editorName.value);
console.log("Edit points button clicked"); formData.append("email", editorEmail.value);
const row = button.closest("tr"); formData.append("member_id", editorMemberId.value);
if (!row) return; formData.append("points", editorPoints.value);
const displayElement = row.querySelector( // Only append resume if a new file is selected
`.points-display-${userId}`, if (editorResume.files && editorResume.files.length > 0) {
) as HTMLDivElement; formData.append("resume", editorResume.files[0]);
const editElement = row.querySelector( } else if (currentUser.resume) {
`.points-edit-${userId}`, // If no new file but there's an existing resume, keep it
) as HTMLDivElement; formData.append("resume", currentUser.resume);
console.log("Display element:", displayElement);
console.log("Edit element:", editElement);
if (displayElement && editElement) {
const currentPoints =
displayElement.querySelector("span")?.textContent;
console.log("Current points:", currentPoints);
const input = editElement.querySelector("input") as HTMLInputElement;
console.log("Input element:", input);
if (input && currentPoints) {
input.value = currentPoints;
}
displayElement.classList.add("hidden");
editElement.classList.remove("hidden");
}
} }
// Handle confirm button click // Log the form data for debugging
if (button.classList.contains("confirm-points")) { console.log("Form data being sent:", {
const row = button.closest("tr"); name: editorName.value,
if (!row) return; email: editorEmail.value,
member_id: editorMemberId.value,
points: editorPoints.value,
hasNewResume: editorResume.files && editorResume.files.length > 0,
hasExistingResume: !!currentUser.resume,
});
const input = row.querySelector( const updatedUser = await this.pb
`input[data-user-id="${userId}"]`, .collection("users")
) as HTMLInputElement; .update(userId, formData);
if (input) { console.log("Update response:", updatedUser);
const points = parseInt(input.value) || 0;
await this.updateUserPoints(userId, points);
const displayElement = row.querySelector( // Close the dialog and refresh the table
`.points-display-${userId}`, profileEditor.close();
) as HTMLDivElement; this.fetchUserResumes();
const editElement = row.querySelector( } catch (err) {
`.points-edit-${userId}`, console.error("Failed to save user profile:", err);
) as HTMLDivElement; }
if (displayElement && editElement) {
displayElement.classList.remove("hidden");
editElement.classList.add("hidden");
}
}
}
// Handle cancel button click
if (button.classList.contains("cancel-points")) {
const row = button.closest("tr");
if (!row) return;
const displayElement = row.querySelector(
`.points-display-${userId}`,
) as HTMLDivElement;
const editElement = row.querySelector(
`.points-edit-${userId}`,
) as HTMLDivElement;
const input = row.querySelector(
`input[data-user-id="${userId}"]`,
) as HTMLInputElement;
const currentPoints =
displayElement?.querySelector("span")?.textContent;
if (input && currentPoints) {
input.value = currentPoints;
}
if (displayElement && editElement) {
displayElement.classList.remove("hidden");
editElement.classList.add("hidden");
}
}
});
} }
private init() { private init() {
@ -883,5 +879,21 @@ export class StoreAuth {
console.log("Auth state changed. IsValid:", this.pb.authStore.isValid); console.log("Auth state changed. IsValid:", this.pb.authStore.isValid);
this.updateUI(); this.updateUI();
}); });
// Profile editor event listeners
const { profileEditor, saveProfileButton } = this.elements;
// Close dialog when clicking outside
profileEditor.addEventListener("click", (e) => {
if (e.target === profileEditor) {
profileEditor.close();
}
});
// Save profile button
saveProfileButton.addEventListener("click", (e) => {
e.preventDefault();
this.handleProfileSave();
});
} }
} }

View file

@ -171,6 +171,76 @@
</div> </div>
</div> </div>
<!-- Profile Editor Dialog -->
<dialog id="profileEditor" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Edit Profile</h3>
<form class="space-y-4" onsubmit="return false" novalidate>
<div class="form-control">
<label class="label">
<span class="label-text">Name</span>
</label>
<input type="text" id="editorName" class="input input-bordered" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
id="editorEmail"
class="input input-bordered"
disabled
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">IEEE Member ID</span>
</label>
<input type="text" id="editorMemberId" class="input input-bordered" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Loyalty Points</span>
</label>
<input
type="number"
id="editorPoints"
min="0"
class="input input-bordered"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Resume</span>
</label>
<div class="flex flex-col gap-2">
<p id="editorCurrentResume" class="text-sm opacity-70">
No resume uploaded
</p>
<input
type="file"
id="editorResume"
accept=".pdf,.doc,.docx"
class="file-input file-input-bordered file-input-sm w-full"
/>
</div>
</div>
<div class="modal-action">
<button type="button" id="saveProfileButton" class="btn btn-primary"
>Save Changes</button
>
<button type="button" class="btn" onclick="profileEditor.close()"
>Cancel</button
>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<style> <style>
.hidden { .hidden {
display: none; display: none;
@ -178,4 +248,10 @@
#userInfo { #userInfo {
transition: opacity 0.3s ease-in-out; transition: opacity 0.3s ease-in-out;
} }
.modal {
background: rgba(0, 0, 0, 0.5);
}
.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
}
</style> </style>

View file

@ -103,7 +103,7 @@ const title = "IEEE Store";
<th>Member ID</th> <th>Member ID</th>
<th>Points</th> <th>Points</th>
<th>Resume</th> <th>Resume</th>
<th>Last Updated</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="resumeList" class="divide-y"> <tbody id="resumeList" class="divide-y">