add officer view

This commit is contained in:
chark1es 2025-01-27 16:14:48 -08:00
parent 53da43fa05
commit 903fe32d9b
4 changed files with 1139 additions and 574 deletions

View file

@ -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) => {

View file

@ -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>

View file

@ -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>