add officer view
This commit is contained in:
parent
53da43fa05
commit
903fe32d9b
4 changed files with 1139 additions and 574 deletions
|
@ -18,6 +18,13 @@ interface AuthElements {
|
|||
memberIdInput: HTMLInputElement;
|
||||
saveMemberId: HTMLButtonElement;
|
||||
memberIdStatus: HTMLParagraphElement;
|
||||
officerViewToggle: HTMLDivElement;
|
||||
officerViewCheckbox: HTMLInputElement;
|
||||
officerContent: HTMLDivElement;
|
||||
resumeList: HTMLTableSectionElement;
|
||||
refreshResumes: HTMLButtonElement;
|
||||
resumeSearch: HTMLInputElement;
|
||||
searchResumes: HTMLButtonElement;
|
||||
}
|
||||
|
||||
export class StoreAuth {
|
||||
|
@ -33,13 +40,19 @@ export class StoreAuth {
|
|||
|
||||
private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } {
|
||||
// Fun typescript fixes
|
||||
const loginButton = document.getElementById("loginButton") as HTMLButtonElement;
|
||||
const logoutButton = document.getElementById("logoutButton") as HTMLButtonElement;
|
||||
const loginButton = document.getElementById(
|
||||
"loginButton",
|
||||
) as HTMLButtonElement;
|
||||
const logoutButton = document.getElementById(
|
||||
"logoutButton",
|
||||
) as HTMLButtonElement;
|
||||
const userInfo = document.getElementById("userInfo") as HTMLDivElement;
|
||||
const loadingSkeleton = document.getElementById("loadingSkeleton") as HTMLDivElement;
|
||||
const loadingSkeleton = document.getElementById(
|
||||
"loadingSkeleton",
|
||||
) as HTMLDivElement;
|
||||
|
||||
// Add CSS for loading state transitions
|
||||
const style = document.createElement('style');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.loading-state {
|
||||
opacity: 0.5;
|
||||
|
@ -51,33 +64,126 @@ export class StoreAuth {
|
|||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const userName = document.getElementById("userName") as HTMLParagraphElement;
|
||||
const userEmail = document.getElementById("userEmail") as HTMLParagraphElement;
|
||||
const memberStatus = document.getElementById("memberStatus") as HTMLDivElement;
|
||||
const lastLogin = document.getElementById("lastLogin") as HTMLParagraphElement;
|
||||
const storeContent = document.getElementById("storeContent") as HTMLDivElement;
|
||||
const resumeUpload = document.getElementById("resumeUpload") as HTMLInputElement;
|
||||
const resumeName = document.getElementById("resumeName") as HTMLParagraphElement;
|
||||
const resumeDownload = document.getElementById("resumeDownload") as HTMLAnchorElement;
|
||||
const deleteResume = document.getElementById("deleteResume") as HTMLButtonElement;
|
||||
const uploadStatus = document.getElementById("uploadStatus") as HTMLParagraphElement;
|
||||
const resumeActions = document.getElementById("resumeActions") as HTMLDivElement;
|
||||
const memberIdInput = document.getElementById("memberIdInput") as HTMLInputElement;
|
||||
const saveMemberId = document.getElementById("saveMemberId") as HTMLButtonElement;
|
||||
const memberIdStatus = document.getElementById("memberIdStatus") as HTMLParagraphElement;
|
||||
const userName = document.getElementById(
|
||||
"userName",
|
||||
) as HTMLParagraphElement;
|
||||
const userEmail = document.getElementById(
|
||||
"userEmail",
|
||||
) as HTMLParagraphElement;
|
||||
const memberStatus = document.getElementById(
|
||||
"memberStatus",
|
||||
) as HTMLDivElement;
|
||||
const lastLogin = document.getElementById(
|
||||
"lastLogin",
|
||||
) as HTMLParagraphElement;
|
||||
const storeContent = document.getElementById(
|
||||
"storeContent",
|
||||
) as HTMLDivElement;
|
||||
const resumeUpload = document.getElementById(
|
||||
"resumeUpload",
|
||||
) as HTMLInputElement;
|
||||
const resumeName = document.getElementById(
|
||||
"resumeName",
|
||||
) as HTMLParagraphElement;
|
||||
const resumeDownload = document.getElementById(
|
||||
"resumeDownload",
|
||||
) as HTMLAnchorElement;
|
||||
const deleteResume = document.getElementById(
|
||||
"deleteResume",
|
||||
) as HTMLButtonElement;
|
||||
const uploadStatus = document.getElementById(
|
||||
"uploadStatus",
|
||||
) as HTMLParagraphElement;
|
||||
const resumeActions = document.getElementById(
|
||||
"resumeActions",
|
||||
) as HTMLDivElement;
|
||||
const memberIdInput = document.getElementById(
|
||||
"memberIdInput",
|
||||
) as HTMLInputElement;
|
||||
const saveMemberId = document.getElementById(
|
||||
"saveMemberId",
|
||||
) as HTMLButtonElement;
|
||||
const memberIdStatus = document.getElementById(
|
||||
"memberIdStatus",
|
||||
) as HTMLParagraphElement;
|
||||
const officerViewToggle = document.getElementById(
|
||||
"officerViewToggle",
|
||||
) as HTMLDivElement;
|
||||
const officerViewCheckbox = officerViewToggle?.querySelector(
|
||||
'input[type="checkbox"]',
|
||||
) as HTMLInputElement;
|
||||
const officerContent = document.getElementById(
|
||||
"officerContent",
|
||||
) as HTMLDivElement;
|
||||
const resumeList = document.getElementById(
|
||||
"resumeList",
|
||||
) as HTMLTableSectionElement;
|
||||
const refreshResumes = document.getElementById(
|
||||
"refreshResumes",
|
||||
) as HTMLButtonElement;
|
||||
const resumeSearch = document.getElementById(
|
||||
"resumeSearch",
|
||||
) as HTMLInputElement;
|
||||
const searchResumes = document.getElementById(
|
||||
"searchResumes",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
if (!loginButton || !logoutButton || !userInfo || !storeContent || !userName || !userEmail ||
|
||||
!memberStatus || !lastLogin || !resumeUpload || !resumeName || !loadingSkeleton ||
|
||||
!resumeDownload || !deleteResume || !uploadStatus || !resumeActions ||
|
||||
!memberIdInput || !saveMemberId || !memberIdStatus) {
|
||||
if (
|
||||
!loginButton ||
|
||||
!logoutButton ||
|
||||
!userInfo ||
|
||||
!storeContent ||
|
||||
!userName ||
|
||||
!userEmail ||
|
||||
!memberStatus ||
|
||||
!lastLogin ||
|
||||
!resumeUpload ||
|
||||
!resumeName ||
|
||||
!loadingSkeleton ||
|
||||
!resumeDownload ||
|
||||
!deleteResume ||
|
||||
!uploadStatus ||
|
||||
!resumeActions ||
|
||||
!memberIdInput ||
|
||||
!saveMemberId ||
|
||||
!memberIdStatus ||
|
||||
!officerViewToggle ||
|
||||
!officerViewCheckbox ||
|
||||
!officerContent ||
|
||||
!resumeList ||
|
||||
!refreshResumes ||
|
||||
!resumeSearch ||
|
||||
!searchResumes
|
||||
) {
|
||||
throw new Error("Required DOM elements not found");
|
||||
}
|
||||
|
||||
return {
|
||||
loginButton, logoutButton, userInfo, userName, userEmail, memberStatus,
|
||||
lastLogin, storeContent, resumeUpload, resumeName, loadingSkeleton,
|
||||
resumeDownload, deleteResume, uploadStatus, resumeActions,
|
||||
memberIdInput, saveMemberId, memberIdStatus
|
||||
loginButton,
|
||||
logoutButton,
|
||||
userInfo,
|
||||
userName,
|
||||
userEmail,
|
||||
memberStatus,
|
||||
lastLogin,
|
||||
storeContent,
|
||||
resumeUpload,
|
||||
resumeName,
|
||||
loadingSkeleton,
|
||||
resumeDownload,
|
||||
deleteResume,
|
||||
uploadStatus,
|
||||
resumeActions,
|
||||
memberIdInput,
|
||||
saveMemberId,
|
||||
memberIdStatus,
|
||||
officerViewToggle,
|
||||
officerViewCheckbox,
|
||||
officerContent,
|
||||
resumeList,
|
||||
refreshResumes,
|
||||
resumeSearch,
|
||||
searchResumes,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -102,13 +208,29 @@ export class StoreAuth {
|
|||
}
|
||||
|
||||
private async updateUI() {
|
||||
const { loginButton, logoutButton, userInfo, userName, userEmail, memberStatus,
|
||||
lastLogin, storeContent, resumeName, resumeDownload, resumeActions,
|
||||
memberIdInput, saveMemberId, resumeUpload, loadingSkeleton } = this.elements;
|
||||
const {
|
||||
loginButton,
|
||||
logoutButton,
|
||||
userInfo,
|
||||
userName,
|
||||
userEmail,
|
||||
memberStatus,
|
||||
lastLogin,
|
||||
storeContent,
|
||||
resumeName,
|
||||
resumeDownload,
|
||||
resumeActions,
|
||||
memberIdInput,
|
||||
saveMemberId,
|
||||
resumeUpload,
|
||||
loadingSkeleton,
|
||||
officerViewToggle,
|
||||
officerContent,
|
||||
} = this.elements;
|
||||
|
||||
// Hide buttons initially
|
||||
loginButton.style.display = 'none';
|
||||
logoutButton.style.display = 'none';
|
||||
loginButton.style.display = "none";
|
||||
logoutButton.style.display = "none";
|
||||
|
||||
if (this.pb.authStore.isValid && this.pb.authStore.model) {
|
||||
// Update all the user information first
|
||||
|
@ -121,11 +243,14 @@ export class StoreAuth {
|
|||
// Check and update member_type if not set
|
||||
if (!user.member_type) {
|
||||
try {
|
||||
const isIeeeOfficer = user.email?.toLowerCase().endsWith('@ieeeucsd.org') || false;
|
||||
const newMemberType = isIeeeOfficer ? "IEEE Officer" : "Regular Member";
|
||||
const isIeeeOfficer =
|
||||
user.email?.toLowerCase().endsWith("@ieeeucsd.org") || false;
|
||||
const newMemberType = isIeeeOfficer
|
||||
? "IEEE Officer"
|
||||
: "Regular Member";
|
||||
|
||||
await this.pb.collection("users").update(user.id, {
|
||||
member_type: newMemberType
|
||||
member_type: newMemberType,
|
||||
});
|
||||
|
||||
user.member_type = newMemberType;
|
||||
|
@ -135,7 +260,13 @@ export class StoreAuth {
|
|||
}
|
||||
|
||||
memberStatus.textContent = user.member_type || "Regular Member";
|
||||
memberStatus.classList.remove("badge-neutral", "badge-success", "badge-warning", "badge-info", "badge-error");
|
||||
memberStatus.classList.remove(
|
||||
"badge-neutral",
|
||||
"badge-success",
|
||||
"badge-warning",
|
||||
"badge-info",
|
||||
"badge-error",
|
||||
);
|
||||
|
||||
// Set color based on member type
|
||||
if (user.member_type === "IEEE Administrator") {
|
||||
|
@ -147,7 +278,12 @@ export class StoreAuth {
|
|||
}
|
||||
} else {
|
||||
memberStatus.textContent = "Not Verified";
|
||||
memberStatus.classList.remove("badge-info", "badge-warning", "badge-success", "badge-error");
|
||||
memberStatus.classList.remove(
|
||||
"badge-info",
|
||||
"badge-warning",
|
||||
"badge-success",
|
||||
"badge-error",
|
||||
);
|
||||
memberStatus.classList.add("badge-neutral");
|
||||
}
|
||||
|
||||
|
@ -156,36 +292,60 @@ export class StoreAuth {
|
|||
this.updateMemberIdState();
|
||||
|
||||
// Update last login
|
||||
const lastLoginDate = user.last_login ? new Date(user.last_login).toLocaleString() : "Never";
|
||||
const lastLoginDate = user.last_login
|
||||
? new Date(user.last_login).toLocaleString()
|
||||
: "Never";
|
||||
lastLogin.textContent = lastLoginDate;
|
||||
|
||||
// Update resume section
|
||||
if (user.resume && (!Array.isArray(user.resume) || user.resume.length > 0)) {
|
||||
if (
|
||||
user.resume &&
|
||||
(!Array.isArray(user.resume) || user.resume.length > 0)
|
||||
) {
|
||||
const resumeUrl = user.resume.toString();
|
||||
resumeName.textContent = this.getFileNameFromUrl(resumeUrl);
|
||||
resumeDownload.href = this.pb.files.getURL(user, resumeUrl);
|
||||
resumeActions.style.display = 'flex';
|
||||
resumeActions.style.display = "flex";
|
||||
} else {
|
||||
resumeName.textContent = "No resume uploaded";
|
||||
resumeDownload.href = "#";
|
||||
resumeActions.style.display = 'none';
|
||||
resumeActions.style.display = "none";
|
||||
}
|
||||
|
||||
// Handle officer view toggle visibility and data loading
|
||||
const isOfficer = [
|
||||
"IEEE Officer",
|
||||
"IEEE Administrator",
|
||||
"IEEE Events",
|
||||
].includes(user.member_type || "");
|
||||
|
||||
officerViewToggle.style.display = isOfficer ? "block" : "none";
|
||||
|
||||
// If user is an officer, preload the table data
|
||||
if (isOfficer) {
|
||||
await this.fetchUserResumes();
|
||||
}
|
||||
|
||||
// After everything is updated, show the content
|
||||
loadingSkeleton.style.display = 'none';
|
||||
userInfo.classList.remove('hidden');
|
||||
loadingSkeleton.style.display = "none";
|
||||
userInfo.classList.remove("hidden");
|
||||
// Use a small delay to ensure the transition works
|
||||
setTimeout(() => {
|
||||
userInfo.style.opacity = '1';
|
||||
userInfo.style.opacity = "1";
|
||||
}, 50);
|
||||
|
||||
logoutButton.style.display = 'block';
|
||||
logoutButton.style.display = "block";
|
||||
} else {
|
||||
// Update for logged out state
|
||||
userName.textContent = "Not signed in";
|
||||
userEmail.textContent = "Not signed in";
|
||||
memberStatus.textContent = "Not verified";
|
||||
memberStatus.classList.remove("badge-info", "badge-warning", "badge-success", "badge-error");
|
||||
memberStatus.classList.remove(
|
||||
"badge-info",
|
||||
"badge-warning",
|
||||
"badge-success",
|
||||
"badge-error",
|
||||
);
|
||||
memberStatus.classList.add("badge-neutral");
|
||||
lastLogin.textContent = "Never";
|
||||
|
||||
|
@ -198,17 +358,18 @@ export class StoreAuth {
|
|||
// Reset resume section
|
||||
resumeName.textContent = "No resume uploaded";
|
||||
resumeDownload.href = "#";
|
||||
resumeActions.style.display = 'none';
|
||||
resumeActions.style.display = "none";
|
||||
|
||||
// After everything is updated, show the content
|
||||
loadingSkeleton.style.display = 'none';
|
||||
userInfo.classList.remove('hidden');
|
||||
loadingSkeleton.style.display = "none";
|
||||
userInfo.classList.remove("hidden");
|
||||
// Use a small delay to ensure the transition works
|
||||
setTimeout(() => {
|
||||
userInfo.style.opacity = '1';
|
||||
userInfo.style.opacity = "1";
|
||||
}, 50);
|
||||
|
||||
loginButton.style.display = 'block';
|
||||
loginButton.style.display = "block";
|
||||
officerViewToggle.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,7 +404,7 @@ export class StoreAuth {
|
|||
}
|
||||
|
||||
await this.pb.collection("users").update(user.id, {
|
||||
member_id: memberId
|
||||
member_id: memberId,
|
||||
});
|
||||
|
||||
memberIdStatus.textContent = "IEEE Member ID saved successfully!";
|
||||
|
@ -256,7 +417,8 @@ export class StoreAuth {
|
|||
}, 3000);
|
||||
} catch (err: any) {
|
||||
console.error("IEEE Member ID save error:", err);
|
||||
memberIdStatus.textContent = "Failed to save IEEE Member ID. Please try again.";
|
||||
memberIdStatus.textContent =
|
||||
"Failed to save IEEE Member ID. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,7 +466,7 @@ export class StoreAuth {
|
|||
}
|
||||
|
||||
await this.pb.collection("users").update(user.id, {
|
||||
"resume": null
|
||||
resume: null,
|
||||
});
|
||||
|
||||
uploadStatus.textContent = "Resume deleted successfully!";
|
||||
|
@ -325,7 +487,7 @@ export class StoreAuth {
|
|||
try {
|
||||
const authMethods = await this.pb.collection("users").listAuthMethods();
|
||||
const oidcProvider = authMethods.oauth2?.providers?.find(
|
||||
(p: { name: string }) => p.name === "oidc"
|
||||
(p: { name: string }) => p.name === "oidc",
|
||||
);
|
||||
|
||||
if (!oidcProvider) {
|
||||
|
@ -351,13 +513,316 @@ export class StoreAuth {
|
|||
this.updateUI();
|
||||
}
|
||||
|
||||
private async fetchUserResumes(searchQuery: string = "") {
|
||||
try {
|
||||
let filter = 'resume != ""';
|
||||
if (searchQuery) {
|
||||
const terms = searchQuery
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((term) => term.length > 0);
|
||||
if (terms.length > 0) {
|
||||
const searchConditions = terms
|
||||
.map(
|
||||
(term) =>
|
||||
`(name ?~ "${term}" || email ?~ "${term}" || member_id ?~ "${term}")`,
|
||||
)
|
||||
.join(" && ");
|
||||
filter += ` && (${searchConditions})`;
|
||||
}
|
||||
}
|
||||
|
||||
const records = await this.pb.collection("users").getList(1, 50, {
|
||||
filter,
|
||||
sort: "-updated",
|
||||
fields: "id,name,email,member_id,resume,updated,points",
|
||||
});
|
||||
|
||||
const { resumeList } = this.elements;
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
if (records.items.length === 0) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td colspan="6" class="text-center py-4">
|
||||
${searchQuery ? "No users found matching your search." : "No resumes uploaded yet."}
|
||||
</td>
|
||||
`;
|
||||
fragment.appendChild(row);
|
||||
} else {
|
||||
records.items.forEach((user) => {
|
||||
const row = document.createElement("tr");
|
||||
const resumeUrl = user.resume
|
||||
? this.pb.files.getURL(user, user.resume)
|
||||
: null;
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="block lg:table-cell">
|
||||
<!-- Mobile View -->
|
||||
<div class="lg:hidden space-y-2">
|
||||
<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">ID: ${user.member_id || "N/A"}</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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">
|
||||
${
|
||||
resumeUrl
|
||||
? `
|
||||
<a href="${resumeUrl}" target="_blank" class="btn btn-ghost btn-xs">
|
||||
View Resume
|
||||
</a>
|
||||
`
|
||||
: '<span class="text-sm opacity-50">No resume</span>'
|
||||
}
|
||||
<span class="text-xs opacity-50">
|
||||
${new Date(user.updated).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop View -->
|
||||
<span class="hidden lg:block">${user.name || "N/A"}</span>
|
||||
</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 w-[140px]">
|
||||
<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">
|
||||
${
|
||||
resumeUrl
|
||||
? `
|
||||
<a href="${resumeUrl}" target="_blank" class="btn btn-ghost btn-xs">
|
||||
View Resume
|
||||
</a>
|
||||
`
|
||||
: '<span class="text-sm opacity-50">No resume</span>'
|
||||
}
|
||||
</td>
|
||||
<td class="hidden lg:table-cell">${new Date(user.updated).toLocaleDateString()}</td>
|
||||
`;
|
||||
|
||||
fragment.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
resumeList.innerHTML = "";
|
||||
resumeList.appendChild(fragment);
|
||||
|
||||
// Setup event listeners for the points editing functionality
|
||||
this.setupPointsEventListeners();
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user resumes:", err);
|
||||
const { resumeList } = this.elements;
|
||||
resumeList.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4 text-error">
|
||||
Failed to fetch resumes. Please try again.
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateUserPoints(userId: string, points: number) {
|
||||
try {
|
||||
await this.pb.collection("users").update(userId, {
|
||||
points: points,
|
||||
});
|
||||
|
||||
// Update the display after successful update
|
||||
const displayElement = document.querySelector(
|
||||
`.points-display-${userId}`,
|
||||
) as HTMLDivElement;
|
||||
const editElement = document.querySelector(
|
||||
`.points-edit-${userId}`,
|
||||
) as HTMLDivElement;
|
||||
if (displayElement && editElement) {
|
||||
const pointsSpan = displayElement.querySelector("span");
|
||||
if (pointsSpan) {
|
||||
pointsSpan.textContent = points.toString();
|
||||
}
|
||||
displayElement.classList.remove("hidden");
|
||||
editElement.classList.add("hidden");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to update points:", err);
|
||||
}
|
||||
}
|
||||
|
||||
private setupPointsEventListeners() {
|
||||
// Use event delegation for all points-related actions
|
||||
const { resumeList } = this.elements;
|
||||
|
||||
resumeList.addEventListener("click", async (e) => {
|
||||
console.log("Click event triggered");
|
||||
const target = e.target as HTMLElement;
|
||||
const button = target.closest("button");
|
||||
console.log("Button found:", button);
|
||||
if (!button) return;
|
||||
|
||||
const userId = button.dataset.userId;
|
||||
console.log("User ID:", userId);
|
||||
if (!userId) return;
|
||||
|
||||
// Handle edit button click
|
||||
if (button.classList.contains("edit-points")) {
|
||||
console.log("Edit points button clicked");
|
||||
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;
|
||||
|
||||
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
|
||||
if (button.classList.contains("confirm-points")) {
|
||||
const row = button.closest("tr");
|
||||
if (!row) return;
|
||||
|
||||
const input = row.querySelector(
|
||||
`input[data-user-id="${userId}"]`,
|
||||
) as HTMLInputElement;
|
||||
if (input) {
|
||||
const points = parseInt(input.value) || 0;
|
||||
await this.updateUserPoints(userId, points);
|
||||
|
||||
const displayElement = row.querySelector(
|
||||
`.points-display-${userId}`,
|
||||
) as HTMLDivElement;
|
||||
const editElement = row.querySelector(
|
||||
`.points-edit-${userId}`,
|
||||
) 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() {
|
||||
// Initial UI update with loading state
|
||||
this.updateUI().catch(console.error);
|
||||
|
||||
// Setup event listeners
|
||||
this.elements.loginButton.addEventListener("click", () => this.handleLogin());
|
||||
this.elements.logoutButton.addEventListener("click", () => this.handleLogout());
|
||||
this.elements.loginButton.addEventListener("click", () =>
|
||||
this.handleLogin(),
|
||||
);
|
||||
this.elements.logoutButton.addEventListener("click", () =>
|
||||
this.handleLogout(),
|
||||
);
|
||||
|
||||
// Resume upload event listener
|
||||
this.elements.resumeUpload.addEventListener("change", (e) => {
|
||||
|
@ -368,10 +833,50 @@ export class StoreAuth {
|
|||
});
|
||||
|
||||
// Resume delete event listener
|
||||
this.elements.deleteResume.addEventListener("click", () => this.handleResumeDelete());
|
||||
this.elements.deleteResume.addEventListener("click", () =>
|
||||
this.handleResumeDelete(),
|
||||
);
|
||||
|
||||
// Member ID save event listener
|
||||
this.elements.saveMemberId.addEventListener("click", () => this.handleMemberIdButton());
|
||||
this.elements.saveMemberId.addEventListener("click", () =>
|
||||
this.handleMemberIdButton(),
|
||||
);
|
||||
|
||||
// Search functionality with minimal debounce
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
const handleSearch = () => {
|
||||
const searchQuery = this.elements.resumeSearch.value.trim();
|
||||
this.fetchUserResumes(searchQuery);
|
||||
};
|
||||
|
||||
// Real-time search with minimal debounce
|
||||
this.elements.resumeSearch.addEventListener("input", () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(handleSearch, 150); // Reduced to 150ms for faster response
|
||||
});
|
||||
|
||||
// Keep the click handler for the search button
|
||||
this.elements.searchResumes.addEventListener("click", handleSearch);
|
||||
|
||||
// Officer view toggle event listener - now just toggles visibility
|
||||
this.elements.officerViewCheckbox.addEventListener("change", (e) => {
|
||||
const isChecked = (e.target as HTMLInputElement).checked;
|
||||
const storeItemsContainer = document.querySelector(
|
||||
".grid.grid-cols-1.lg\\:grid-cols-2.xl\\:grid-cols-3",
|
||||
) as HTMLElement;
|
||||
const { officerContent } = this.elements;
|
||||
|
||||
if (storeItemsContainer) {
|
||||
storeItemsContainer.style.display = isChecked ? "none" : "grid";
|
||||
}
|
||||
officerContent.style.display = isChecked ? "block" : "none";
|
||||
});
|
||||
|
||||
// Refresh resumes button event listener
|
||||
this.elements.refreshResumes.addEventListener("click", () => {
|
||||
this.elements.resumeSearch.value = ""; // Clear search when refreshing
|
||||
this.fetchUserResumes();
|
||||
});
|
||||
|
||||
// Listen for auth state changes
|
||||
this.pb.authStore.onChange(async (token) => {
|
||||
|
|
|
@ -79,16 +79,12 @@
|
|||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm opacity-70">Name</label>
|
||||
<p id="userName" class="h-[1.75rem] font-medium">
|
||||
Not signed in
|
||||
</p>
|
||||
<p id="userName" class="h-[1.75rem] font-medium">Not signed in</p>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm opacity-70">Email</label>
|
||||
<p id="userEmail" class="h-[1.75rem] font-medium">
|
||||
Not signed in
|
||||
</p>
|
||||
<p id="userEmail" class="h-[1.75rem] font-medium">Not signed in</p>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
<div class="space-y-1">
|
||||
|
@ -99,6 +95,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="officerViewToggle" class="hidden">
|
||||
<label
|
||||
class="flex items-center justify-between w-full px-1 bg-base-200 rounded-lg"
|
||||
>
|
||||
<span class="text-sm">Officer View</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm opacity-70">IEEE Member ID</label>
|
||||
|
@ -109,34 +113,23 @@
|
|||
placeholder="Enter your IEEE Member ID"
|
||||
class="input input-bordered w-full h-8 min-h-[2rem] disabled:bg-base-300 disabled:border-2 disabled:border-opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
id="saveMemberId"
|
||||
class="btn btn-primary h-8 min-h-[2rem]"
|
||||
<button id="saveMemberId" class="btn btn-primary h-8 min-h-[2rem]"
|
||||
>Save</button
|
||||
>
|
||||
</div>
|
||||
<p id="memberIdStatus" class="text-xs mt-1 opacity-70">
|
||||
</p>
|
||||
<p id="memberIdStatus" class="text-xs mt-1 opacity-70"></p>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm opacity-70">Last Login</label>
|
||||
<p
|
||||
id="lastLogin"
|
||||
class="text-sm h-[1.25rem] opacity-80"
|
||||
>
|
||||
Never
|
||||
</p>
|
||||
<p id="lastLogin" class="text-sm h-[1.25rem] opacity-80">Never</p>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm opacity-70">Resume</label>
|
||||
<div id="resumeSection" class="space-y-2">
|
||||
<div class="flex items-center gap-2 h-[1.25rem]">
|
||||
<p
|
||||
id="resumeName"
|
||||
class="text-sm truncate flex-1"
|
||||
>
|
||||
<p id="resumeName" class="text-sm truncate flex-1">
|
||||
No resume uploaded
|
||||
</p>
|
||||
<div id="resumeActions" class="flex gap-2">
|
||||
|
@ -148,8 +141,7 @@
|
|||
>
|
||||
<button
|
||||
id="deleteResume"
|
||||
class="btn btn-ghost btn-xs text-error"
|
||||
>Delete</button
|
||||
class="btn btn-ghost btn-xs text-error">Delete</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -160,11 +152,7 @@
|
|||
accept=".pdf,.doc,.docx"
|
||||
class="file-input file-input-bordered file-input-sm w-full"
|
||||
/>
|
||||
<p
|
||||
id="uploadStatus"
|
||||
class="text-xs mt-1 opacity-70"
|
||||
>
|
||||
</p>
|
||||
<p id="uploadStatus" class="text-xs mt-1 opacity-70"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,19 +6,17 @@ const title = "IEEE Store";
|
|||
---
|
||||
|
||||
<Layout {title}>
|
||||
<main class="w-[95%] mx-auto pb-12 md:pt-[14vh] pt-[12vw] min-h-screen">
|
||||
<main class="w-[95%] mx-auto pb-12 md:pt-[5vh] pt-[5vw] min-h-screen">
|
||||
<h1 class="text-4xl font-bold mb-12">IEEE UCSD Store</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Left Column - User Info -->
|
||||
<div class="md:col-span-1 h-fit">
|
||||
<div class="lg:col-span-2 xl:col-span-1 h-fit">
|
||||
<UserProfile />
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Store Items -->
|
||||
<div id="storeContent" class="md:col-span-3">
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
<div id="storeContent" class="lg:col-span-2 xl:col-span-3">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<StoreItem
|
||||
name="Item Name"
|
||||
description="Description of the item goes here. This is a placeholder."
|
||||
|
@ -40,6 +38,80 @@ const title = "IEEE Store";
|
|||
price={15.0}
|
||||
/>
|
||||
</div>
|
||||
<!-- Officer View Content -->
|
||||
<div id="officerContent" class="hidden">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 lg:gap-0 mb-4"
|
||||
>
|
||||
<h2 class="text-2xl font-bold">Member Management</h2>
|
||||
<div class="flex flex-col lg:flex-row gap-2">
|
||||
<div class="form-control w-full">
|
||||
<input
|
||||
type="text"
|
||||
id="resumeSearch"
|
||||
placeholder="Search users..."
|
||||
class="input input-bordered input-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 w-full lg:w-auto">
|
||||
<button
|
||||
id="searchResumes"
|
||||
class="btn btn-sm flex-1 lg:flex-none"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<span class="lg:hidden ml-2">Search</span>
|
||||
</button>
|
||||
<button
|
||||
id="refreshResumes"
|
||||
class="btn btn-ghost btn-sm flex-1 lg:flex-none"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="lg:hidden ml-2">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<!-- Use responsive classes -->
|
||||
<thead class="hidden lg:table-header-group">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Member ID</th>
|
||||
<th>Points</th>
|
||||
<th>Resume</th>
|
||||
<th>Last Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="resumeList" class="divide-y">
|
||||
<!-- Resume entries will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
Loading…
Reference in a new issue