add config, pdf, and partial documentation support
This commit is contained in:
parent
08a6a8a15c
commit
4f31592aa1
3 changed files with 292 additions and 54 deletions
|
@ -1,4 +1,80 @@
|
||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
import configYaml from "../../data/storeConfig.yaml?raw";
|
||||||
|
|
||||||
|
// Configuration type definitions
|
||||||
|
interface Role {
|
||||||
|
name: string;
|
||||||
|
badge: string;
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
api: {
|
||||||
|
baseUrl: string;
|
||||||
|
oauth2: {
|
||||||
|
redirectPath: string;
|
||||||
|
providerName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
roles: {
|
||||||
|
administrator: Role;
|
||||||
|
officer: Role;
|
||||||
|
sponsor: Role;
|
||||||
|
member: Role;
|
||||||
|
};
|
||||||
|
resume: {
|
||||||
|
allowedTypes: string[];
|
||||||
|
maxSize: number;
|
||||||
|
viewer: {
|
||||||
|
width: string;
|
||||||
|
maxWidth: string;
|
||||||
|
height: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ui: {
|
||||||
|
transitions: {
|
||||||
|
fadeDelay: number;
|
||||||
|
};
|
||||||
|
messages: {
|
||||||
|
memberId: {
|
||||||
|
saving: string;
|
||||||
|
success: string;
|
||||||
|
error: string;
|
||||||
|
messageTimeout: number;
|
||||||
|
};
|
||||||
|
resume: {
|
||||||
|
uploading: string;
|
||||||
|
success: string;
|
||||||
|
error: string;
|
||||||
|
deleting: string;
|
||||||
|
deleteSuccess: string;
|
||||||
|
deleteError: string;
|
||||||
|
messageTimeout: number;
|
||||||
|
};
|
||||||
|
auth: {
|
||||||
|
loginError: string;
|
||||||
|
notSignedIn: string;
|
||||||
|
notVerified: string;
|
||||||
|
notProvided: string;
|
||||||
|
notAvailable: string;
|
||||||
|
never: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
defaults: {
|
||||||
|
pageSize: number;
|
||||||
|
sortField: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
autoDetection: {
|
||||||
|
officer: {
|
||||||
|
emailDomain: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse YAML configuration with type
|
||||||
|
const config = yaml.load(configYaml) as Config;
|
||||||
|
|
||||||
interface AuthElements {
|
interface AuthElements {
|
||||||
loginButton: HTMLButtonElement;
|
loginButton: HTMLButtonElement;
|
||||||
|
@ -34,6 +110,10 @@ interface AuthElements {
|
||||||
editorCurrentResume: HTMLParagraphElement;
|
editorCurrentResume: HTMLParagraphElement;
|
||||||
saveProfileButton: HTMLButtonElement;
|
saveProfileButton: HTMLButtonElement;
|
||||||
sponsorViewToggle: HTMLDivElement;
|
sponsorViewToggle: HTMLDivElement;
|
||||||
|
pdfViewer: HTMLDialogElement;
|
||||||
|
pdfFrame: HTMLIFrameElement;
|
||||||
|
pdfTitle: HTMLHeadingElement;
|
||||||
|
pdfExternalLink: HTMLAnchorElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StoreAuth {
|
export class StoreAuth {
|
||||||
|
@ -41,9 +121,10 @@ export class StoreAuth {
|
||||||
private elements: AuthElements & { loadingSkeleton: HTMLDivElement };
|
private elements: AuthElements & { loadingSkeleton: HTMLDivElement };
|
||||||
private isEditingMemberId: boolean = false;
|
private isEditingMemberId: boolean = false;
|
||||||
private cachedUsers: any[] = []; // Store users data
|
private cachedUsers: any[] = []; // Store users data
|
||||||
|
private config = config;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.pb = new PocketBase("https://pocketbase.ieeeucsd.org");
|
this.pb = new PocketBase(this.config.api.baseUrl);
|
||||||
this.elements = this.getElements();
|
this.elements = this.getElements();
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
@ -167,6 +248,11 @@ export class StoreAuth {
|
||||||
"sponsorViewToggle",
|
"sponsorViewToggle",
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
|
|
||||||
|
const pdfViewer = document.getElementById("pdfViewer") as HTMLDialogElement;
|
||||||
|
const pdfFrame = document.getElementById("pdfFrame") as HTMLIFrameElement;
|
||||||
|
const pdfTitle = document.getElementById("pdfTitle") as HTMLHeadingElement;
|
||||||
|
const pdfExternalLink = document.getElementById("pdfExternalLink") as HTMLAnchorElement;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!loginButton ||
|
!loginButton ||
|
||||||
!logoutButton ||
|
!logoutButton ||
|
||||||
|
@ -201,7 +287,11 @@ export class StoreAuth {
|
||||||
!editorResume ||
|
!editorResume ||
|
||||||
!editorCurrentResume ||
|
!editorCurrentResume ||
|
||||||
!saveProfileButton ||
|
!saveProfileButton ||
|
||||||
!sponsorViewToggle
|
!sponsorViewToggle ||
|
||||||
|
!pdfViewer ||
|
||||||
|
!pdfFrame ||
|
||||||
|
!pdfTitle ||
|
||||||
|
!pdfExternalLink
|
||||||
) {
|
) {
|
||||||
throw new Error("Required DOM elements not found");
|
throw new Error("Required DOM elements not found");
|
||||||
}
|
}
|
||||||
|
@ -241,6 +331,10 @@ export class StoreAuth {
|
||||||
editorCurrentResume,
|
editorCurrentResume,
|
||||||
saveProfileButton,
|
saveProfileButton,
|
||||||
sponsorViewToggle,
|
sponsorViewToggle,
|
||||||
|
pdfViewer,
|
||||||
|
pdfFrame,
|
||||||
|
pdfTitle,
|
||||||
|
pdfExternalLink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,10 +387,10 @@ export class StoreAuth {
|
||||||
if (this.pb.authStore.isValid && this.pb.authStore.model) {
|
if (this.pb.authStore.isValid && this.pb.authStore.model) {
|
||||||
// Update all the user information first
|
// Update all the user information first
|
||||||
const user = this.pb.authStore.model;
|
const user = this.pb.authStore.model;
|
||||||
const isSponsor = user.member_type === "IEEE Sponsor";
|
const isSponsor = user.member_type === this.config.roles.sponsor.name;
|
||||||
|
|
||||||
userName.textContent = user.name || "Name not provided";
|
userName.textContent = user.name || this.config.ui.messages.auth.notProvided;
|
||||||
userEmail.textContent = user.email || "Email not available";
|
userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable;
|
||||||
|
|
||||||
// Hide member ID and resume sections for sponsors
|
// Hide member ID and resume sections for sponsors
|
||||||
const memberIdSection = memberIdInput.closest('.space-y-1') as HTMLElement;
|
const memberIdSection = memberIdInput.closest('.space-y-1') as HTMLElement;
|
||||||
|
@ -327,11 +421,10 @@ export class StoreAuth {
|
||||||
// Check and update member_type if not set
|
// Check and update member_type if not set
|
||||||
if (!user.member_type) {
|
if (!user.member_type) {
|
||||||
try {
|
try {
|
||||||
const isIeeeOfficer =
|
const isIeeeOfficer = user.email?.toLowerCase().endsWith(this.config.autoDetection.officer.emailDomain) || false;
|
||||||
user.email?.toLowerCase().endsWith("@ieeeucsd.org") || false;
|
|
||||||
const newMemberType = isIeeeOfficer
|
const newMemberType = isIeeeOfficer
|
||||||
? "IEEE Officer"
|
? this.config.roles.officer.name
|
||||||
: "Regular Member";
|
: this.config.roles.member.name;
|
||||||
|
|
||||||
await this.pb.collection("users").update(user.id, {
|
await this.pb.collection("users").update(user.id, {
|
||||||
member_type: newMemberType,
|
member_type: newMemberType,
|
||||||
|
@ -343,7 +436,7 @@ export class StoreAuth {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memberStatus.textContent = user.member_type || "Regular Member";
|
memberStatus.textContent = user.member_type || this.config.roles.member.name;
|
||||||
memberStatus.classList.remove(
|
memberStatus.classList.remove(
|
||||||
"badge-neutral",
|
"badge-neutral",
|
||||||
"badge-success",
|
"badge-success",
|
||||||
|
@ -353,19 +446,19 @@ export class StoreAuth {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set color based on member type
|
// Set color based on member type
|
||||||
if (user.member_type === "IEEE Administrator") {
|
const role = Object.values(this.config.roles).find(r => r.name === user.member_type);
|
||||||
memberStatus.classList.add("badge-warning"); // Red for administrators
|
if (role) {
|
||||||
} else if (user.member_type === "IEEE Officer") {
|
memberStatus.classList.add(role.badge);
|
||||||
memberStatus.classList.add("badge-info"); // Blue for officers
|
|
||||||
} else if (user.member_type === "IEEE Sponsor") {
|
|
||||||
memberStatus.classList.add("badge-warning"); // Yellow for sponsors
|
|
||||||
} else {
|
} else {
|
||||||
memberStatus.classList.add("badge-neutral"); // Neutral for regular members
|
memberStatus.classList.add(this.config.roles.member.badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle view toggles visibility
|
// Handle view toggles visibility
|
||||||
const isOfficer = ["IEEE Officer", "IEEE Administrator"].includes(user.member_type || "");
|
const isOfficer = [
|
||||||
const isSponsor = user.member_type === "IEEE Sponsor";
|
this.config.roles.officer.name,
|
||||||
|
this.config.roles.administrator.name
|
||||||
|
].includes(user.member_type || "");
|
||||||
|
const isSponsor = user.member_type === this.config.roles.sponsor.name;
|
||||||
|
|
||||||
officerViewToggle.style.display = isOfficer ? "block" : "none";
|
officerViewToggle.style.display = isOfficer ? "block" : "none";
|
||||||
sponsorViewToggle.style.display = isSponsor ? "block" : "none";
|
sponsorViewToggle.style.display = isSponsor ? "block" : "none";
|
||||||
|
@ -375,7 +468,7 @@ export class StoreAuth {
|
||||||
await this.fetchUserResumes();
|
await this.fetchUserResumes();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
memberStatus.textContent = "Not Verified";
|
memberStatus.textContent = this.config.ui.messages.auth.notVerified;
|
||||||
memberStatus.classList.remove(
|
memberStatus.classList.remove(
|
||||||
"badge-info",
|
"badge-info",
|
||||||
"badge-warning",
|
"badge-warning",
|
||||||
|
@ -392,7 +485,7 @@ export class StoreAuth {
|
||||||
// Update last login
|
// Update last login
|
||||||
const lastLoginDate = user.last_login
|
const lastLoginDate = user.last_login
|
||||||
? new Date(user.last_login).toLocaleString()
|
? new Date(user.last_login).toLocaleString()
|
||||||
: "Never";
|
: this.config.ui.messages.auth.never;
|
||||||
lastLogin.textContent = lastLoginDate;
|
lastLogin.textContent = lastLoginDate;
|
||||||
|
|
||||||
// Update resume section
|
// Update resume section
|
||||||
|
@ -401,12 +494,19 @@ export class StoreAuth {
|
||||||
(!Array.isArray(user.resume) || user.resume.length > 0)
|
(!Array.isArray(user.resume) || user.resume.length > 0)
|
||||||
) {
|
) {
|
||||||
const resumeUrl = user.resume.toString();
|
const resumeUrl = user.resume.toString();
|
||||||
resumeName.textContent = this.getFileNameFromUrl(resumeUrl);
|
const fileName = this.getFileNameFromUrl(resumeUrl);
|
||||||
resumeDownload.href = this.pb.files.getURL(user, resumeUrl);
|
resumeName.textContent = fileName;
|
||||||
|
const fullUrl = this.pb.files.getURL(user, resumeUrl);
|
||||||
|
resumeDownload.href = "#";
|
||||||
|
resumeDownload.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleResumeView(fullUrl, fileName);
|
||||||
|
};
|
||||||
resumeActions.style.display = "flex";
|
resumeActions.style.display = "flex";
|
||||||
} else {
|
} else {
|
||||||
resumeName.textContent = "No resume uploaded";
|
resumeName.textContent = "No resume uploaded";
|
||||||
resumeDownload.href = "#";
|
resumeDownload.href = "#";
|
||||||
|
resumeDownload.onclick = null;
|
||||||
resumeActions.style.display = "none";
|
resumeActions.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,9 +521,9 @@ export class StoreAuth {
|
||||||
logoutButton.style.display = "block";
|
logoutButton.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
// Update for logged out state
|
// Update for logged out state
|
||||||
userName.textContent = "Not signed in";
|
userName.textContent = this.config.ui.messages.auth.notSignedIn;
|
||||||
userEmail.textContent = "Not signed in";
|
userEmail.textContent = this.config.ui.messages.auth.notSignedIn;
|
||||||
memberStatus.textContent = "Not verified";
|
memberStatus.textContent = this.config.ui.messages.auth.notVerified;
|
||||||
memberStatus.classList.remove(
|
memberStatus.classList.remove(
|
||||||
"badge-info",
|
"badge-info",
|
||||||
"badge-warning",
|
"badge-warning",
|
||||||
|
@ -431,7 +531,7 @@ export class StoreAuth {
|
||||||
"badge-error",
|
"badge-error",
|
||||||
);
|
);
|
||||||
memberStatus.classList.add("badge-neutral");
|
memberStatus.classList.add("badge-neutral");
|
||||||
lastLogin.textContent = "Never";
|
lastLogin.textContent = this.config.ui.messages.auth.never;
|
||||||
|
|
||||||
// Disable member ID input and save button
|
// Disable member ID input and save button
|
||||||
memberIdInput.disabled = true;
|
memberIdInput.disabled = true;
|
||||||
|
@ -448,6 +548,7 @@ export class StoreAuth {
|
||||||
// Reset resume section
|
// Reset resume section
|
||||||
resumeName.textContent = "No resume uploaded";
|
resumeName.textContent = "No resume uploaded";
|
||||||
resumeDownload.href = "#";
|
resumeDownload.href = "#";
|
||||||
|
resumeDownload.onclick = null;
|
||||||
resumeActions.style.display = "none";
|
resumeActions.style.display = "none";
|
||||||
|
|
||||||
// After everything is updated, show the content
|
// After everything is updated, show the content
|
||||||
|
@ -492,7 +593,7 @@ export class StoreAuth {
|
||||||
const memberId = memberIdInput.value.trim();
|
const memberId = memberIdInput.value.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
memberIdStatus.textContent = "Saving member ID...";
|
memberIdStatus.textContent = this.config.ui.messages.memberId.saving;
|
||||||
|
|
||||||
const user = this.pb.authStore.model;
|
const user = this.pb.authStore.model;
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
|
@ -503,26 +604,36 @@ export class StoreAuth {
|
||||||
member_id: memberId,
|
member_id: memberId,
|
||||||
});
|
});
|
||||||
|
|
||||||
memberIdStatus.textContent = "IEEE Member ID saved successfully!";
|
memberIdStatus.textContent = this.config.ui.messages.memberId.success;
|
||||||
this.isEditingMemberId = false;
|
this.isEditingMemberId = false;
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
// Clear the status message after a delay
|
// Clear the status message after a delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
memberIdStatus.textContent = "";
|
memberIdStatus.textContent = "";
|
||||||
}, 3000);
|
}, this.config.ui.messages.memberId.messageTimeout);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("IEEE Member ID save error:", err);
|
console.error("IEEE Member ID save error:", err);
|
||||||
memberIdStatus.textContent =
|
memberIdStatus.textContent = this.config.ui.messages.memberId.error;
|
||||||
"Failed to save IEEE Member ID. Please try again.";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleResumeUpload(file: File) {
|
private async handleResumeUpload(file: File) {
|
||||||
const { uploadStatus } = this.elements;
|
const { uploadStatus } = this.elements;
|
||||||
|
|
||||||
|
// Check file type and size
|
||||||
|
if (!this.config.resume.allowedTypes.some(type => file.name.toLowerCase().endsWith(type))) {
|
||||||
|
uploadStatus.textContent = `File type not allowed. Allowed types: ${this.config.resume.allowedTypes.join(", ")}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > this.config.resume.maxSize) {
|
||||||
|
uploadStatus.textContent = `File too large. Maximum size: ${this.config.resume.maxSize / 1024 / 1024}MB`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uploadStatus.textContent = "Uploading resume...";
|
uploadStatus.textContent = this.config.ui.messages.resume.uploading;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("resume", file);
|
formData.append("resume", file);
|
||||||
|
@ -543,7 +654,7 @@ export class StoreAuth {
|
||||||
|
|
||||||
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 = this.config.ui.messages.resume.success;
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
// Clear the file input
|
// Clear the file input
|
||||||
|
@ -552,10 +663,10 @@ export class StoreAuth {
|
||||||
// Clear the status message after a delay
|
// Clear the status message after a delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uploadStatus.textContent = "";
|
uploadStatus.textContent = "";
|
||||||
}, 3000);
|
}, this.config.ui.messages.resume.messageTimeout);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Resume upload error:", err);
|
console.error("Resume upload error:", err);
|
||||||
uploadStatus.textContent = "Failed to upload resume. Please try again.";
|
uploadStatus.textContent = this.config.ui.messages.resume.error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,7 +674,7 @@ export class StoreAuth {
|
||||||
const { uploadStatus } = this.elements;
|
const { uploadStatus } = this.elements;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uploadStatus.textContent = "Deleting resume...";
|
uploadStatus.textContent = this.config.ui.messages.resume.deleting;
|
||||||
|
|
||||||
const user = this.pb.authStore.model;
|
const user = this.pb.authStore.model;
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
|
@ -574,16 +685,16 @@ export class StoreAuth {
|
||||||
resume: null,
|
resume: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadStatus.textContent = "Resume deleted successfully!";
|
uploadStatus.textContent = this.config.ui.messages.resume.deleteSuccess;
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
// Clear the status message after a delay
|
// Clear the status message after a delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uploadStatus.textContent = "";
|
uploadStatus.textContent = "";
|
||||||
}, 3000);
|
}, this.config.ui.messages.resume.messageTimeout);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Resume deletion error:", err);
|
console.error("Resume deletion error:", err);
|
||||||
uploadStatus.textContent = "Failed to delete resume. Please try again.";
|
uploadStatus.textContent = this.config.ui.messages.resume.deleteError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,7 +703,7 @@ export class StoreAuth {
|
||||||
try {
|
try {
|
||||||
const authMethods = await this.pb.collection("users").listAuthMethods();
|
const authMethods = await this.pb.collection("users").listAuthMethods();
|
||||||
const oidcProvider = authMethods.oauth2?.providers?.find(
|
const oidcProvider = authMethods.oauth2?.providers?.find(
|
||||||
(p: { name: string }) => p.name === "oidc",
|
(p: { name: string }) => p.name === this.config.api.oauth2.providerName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!oidcProvider) {
|
if (!oidcProvider) {
|
||||||
|
@ -603,12 +714,12 @@ export class StoreAuth {
|
||||||
localStorage.setItem("provider", JSON.stringify(oidcProvider));
|
localStorage.setItem("provider", JSON.stringify(oidcProvider));
|
||||||
|
|
||||||
// Redirect to the authorization URL
|
// Redirect to the authorization URL
|
||||||
const redirectUrl = window.location.origin + "/oauth2-redirect";
|
const redirectUrl = window.location.origin + this.config.api.oauth2.redirectPath;
|
||||||
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
|
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
|
||||||
window.location.href = authUrl;
|
window.location.href = authUrl;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Authentication error:", err);
|
console.error("Authentication error:", err);
|
||||||
this.elements.userEmail.textContent = "Failed to start authentication";
|
this.elements.userEmail.textContent = this.config.ui.messages.auth.loginError;
|
||||||
this.elements.userName.textContent = "Error";
|
this.elements.userName.textContent = "Error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -643,8 +754,8 @@ export class StoreAuth {
|
||||||
try {
|
try {
|
||||||
// Only fetch from API if we don't have cached data
|
// Only fetch from API if we don't have cached data
|
||||||
if (this.cachedUsers.length === 0) {
|
if (this.cachedUsers.length === 0) {
|
||||||
const records = await this.pb.collection("users").getList(1, 50, {
|
const records = await this.pb.collection("users").getList(1, this.config.ui.defaults.pageSize, {
|
||||||
sort: "-updated",
|
sort: this.config.ui.defaults.sortField,
|
||||||
fields: "id,name,email,member_id,resume,points,collectionId,collectionName",
|
fields: "id,name,email,member_id,resume,points,collectionId,collectionName",
|
||||||
expand: "resume",
|
expand: "resume",
|
||||||
});
|
});
|
||||||
|
@ -668,7 +779,7 @@ export class StoreAuth {
|
||||||
|
|
||||||
const { resumeList } = this.elements;
|
const { resumeList } = this.elements;
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
const isSponsor = this.pb.authStore.model?.member_type === "IEEE Sponsor";
|
const isSponsor = this.pb.authStore.model?.member_type === this.config.roles.sponsor.name;
|
||||||
|
|
||||||
if (filteredUsers.length === 0) {
|
if (filteredUsers.length === 0) {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
|
@ -684,6 +795,7 @@ export class StoreAuth {
|
||||||
const resumeUrl = user.resume && user.resume !== ""
|
const resumeUrl = user.resume && user.resume !== ""
|
||||||
? this.pb.files.getURL(user, user.resume.toString())
|
? this.pb.files.getURL(user, user.resume.toString())
|
||||||
: null;
|
: null;
|
||||||
|
const fileName = resumeUrl ? this.getFileNameFromUrl(user.resume.toString()) : null;
|
||||||
|
|
||||||
// Create edit button only if not a sponsor
|
// Create edit button only if not a sponsor
|
||||||
const editButton = !isSponsor ? `
|
const editButton = !isSponsor ? `
|
||||||
|
@ -695,6 +807,11 @@ export class StoreAuth {
|
||||||
</button>
|
</button>
|
||||||
` : '';
|
` : '';
|
||||||
|
|
||||||
|
// Create view resume link
|
||||||
|
const viewResumeLink = resumeUrl
|
||||||
|
? `<a href="#" class="btn btn-ghost btn-xs" onclick="event.preventDefault(); document.dispatchEvent(new CustomEvent('viewResume', { detail: { url: '${resumeUrl}', fileName: '${fileName}' } }));">View Resume</a>`
|
||||||
|
: '<span class="text-sm opacity-50">No resume</span>';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td class="block lg:table-cell">
|
<td class="block lg:table-cell">
|
||||||
<!-- Mobile View -->
|
<!-- Mobile View -->
|
||||||
|
@ -704,10 +821,7 @@ export class StoreAuth {
|
||||||
<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="text-sm opacity-70">Points: ${user.points || 0}</div>
|
<div class="text-sm opacity-70">Points: ${user.points || 0}</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
${resumeUrl
|
${viewResumeLink}
|
||||||
? `<a href="${resumeUrl}" target="_blank" class="btn btn-ghost btn-xs">View Resume</a>`
|
|
||||||
: '<span class="text-sm opacity-50">No resume</span>'
|
|
||||||
}
|
|
||||||
${editButton}
|
${editButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -719,10 +833,7 @@ export class StoreAuth {
|
||||||
<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">${user.points || 0}</td>
|
<td class="hidden lg:table-cell">${user.points || 0}</td>
|
||||||
<td class="hidden lg:table-cell">
|
<td class="hidden lg:table-cell">
|
||||||
${resumeUrl
|
${viewResumeLink}
|
||||||
? `<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>
|
||||||
<td class="hidden lg:table-cell">
|
<td class="hidden lg:table-cell">
|
||||||
${editButton}
|
${editButton}
|
||||||
|
@ -859,6 +970,14 @@ export class StoreAuth {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleResumeView(url: string, fileName: string) {
|
||||||
|
const { pdfViewer, pdfFrame, pdfTitle, pdfExternalLink } = this.elements;
|
||||||
|
pdfFrame.src = url;
|
||||||
|
pdfTitle.textContent = fileName;
|
||||||
|
pdfExternalLink.href = url;
|
||||||
|
pdfViewer.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Initial UI update with loading state
|
// Initial UI update with loading state
|
||||||
this.updateUI().catch(console.error);
|
this.updateUI().catch(console.error);
|
||||||
|
@ -969,5 +1088,10 @@ export class StoreAuth {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleProfileSave();
|
this.handleProfileSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add resume view event listener
|
||||||
|
document.addEventListener('viewResume', ((e: CustomEvent) => {
|
||||||
|
this.handleResumeView(e.detail.url, e.detail.fileName);
|
||||||
|
}) as EventListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,6 +289,50 @@
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- PDF Viewer Modal -->
|
||||||
|
<dialog id="pdfViewer" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-5xl h-[80vh]">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="font-bold text-lg" id="pdfTitle">Resume</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
id="pdfExternalLink"
|
||||||
|
href="#"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4 mr-1"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Open in New Tab
|
||||||
|
</a>
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost">✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-[calc(100%-4rem)]">
|
||||||
|
<iframe
|
||||||
|
id="pdfFrame"
|
||||||
|
class="w-full h-full rounded-lg border-2 border-base-300"
|
||||||
|
src=""></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
70
src/data/storeConfig.yaml
Normal file
70
src/data/storeConfig.yaml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
api:
|
||||||
|
baseUrl: https://pocketbase.ieeeucsd.org
|
||||||
|
oauth2:
|
||||||
|
redirectPath: /oauth2-redirect
|
||||||
|
providerName: oidc
|
||||||
|
|
||||||
|
roles:
|
||||||
|
administrator:
|
||||||
|
name: IEEE Administrator
|
||||||
|
badge: badge-warning
|
||||||
|
permissions: [view, edit, manage]
|
||||||
|
|
||||||
|
officer:
|
||||||
|
name: IEEE Officer
|
||||||
|
badge: badge-info
|
||||||
|
permissions: [view, edit]
|
||||||
|
|
||||||
|
sponsor:
|
||||||
|
name: IEEE Sponsor
|
||||||
|
badge: badge-warning
|
||||||
|
permissions: [view]
|
||||||
|
|
||||||
|
member:
|
||||||
|
name: Regular Member
|
||||||
|
badge: badge-neutral
|
||||||
|
permissions: [self]
|
||||||
|
|
||||||
|
resume:
|
||||||
|
allowedTypes: [.pdf, .doc, .docx]
|
||||||
|
maxSize: 5242880 # 5MB in bytes
|
||||||
|
viewer:
|
||||||
|
width: w-11/12
|
||||||
|
maxWidth: max-w-5xl
|
||||||
|
height: h-[80vh]
|
||||||
|
|
||||||
|
ui:
|
||||||
|
transitions:
|
||||||
|
fadeDelay: 50
|
||||||
|
|
||||||
|
messages:
|
||||||
|
memberId:
|
||||||
|
saving: Saving member ID...
|
||||||
|
success: IEEE Member ID saved successfully!
|
||||||
|
error: Failed to save IEEE Member ID. Please try again.
|
||||||
|
messageTimeout: 3000
|
||||||
|
|
||||||
|
resume:
|
||||||
|
uploading: Uploading resume...
|
||||||
|
success: Resume uploaded successfully!
|
||||||
|
error: Failed to upload resume. Please try again.
|
||||||
|
deleting: Deleting resume...
|
||||||
|
deleteSuccess: Resume deleted successfully!
|
||||||
|
deleteError: Failed to delete resume. Please try again.
|
||||||
|
messageTimeout: 3000
|
||||||
|
|
||||||
|
auth:
|
||||||
|
loginError: Failed to start authentication
|
||||||
|
notSignedIn: Not signed in
|
||||||
|
notVerified: Not verified
|
||||||
|
notProvided: Not provided
|
||||||
|
notAvailable: Not available
|
||||||
|
never: Never
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
pageSize: 50
|
||||||
|
sortField: -updated
|
||||||
|
|
||||||
|
autoDetection:
|
||||||
|
officer:
|
||||||
|
emailDomain: "@ieeeucsd.org"
|
Loading…
Reference in a new issue