move settings around
This commit is contained in:
parent
eea944fcd2
commit
49e1b00586
4 changed files with 380 additions and 382 deletions
|
@ -85,41 +85,20 @@ interface AuthElements {
|
|||
memberStatus: HTMLDivElement;
|
||||
lastLogin: HTMLParagraphElement;
|
||||
storeContent: HTMLDivElement;
|
||||
resumeUpload: HTMLInputElement;
|
||||
resumeName: HTMLParagraphElement;
|
||||
resumeDownload: HTMLAnchorElement;
|
||||
deleteResume: HTMLButtonElement;
|
||||
uploadStatus: HTMLParagraphElement;
|
||||
resumeActions: HTMLDivElement;
|
||||
memberIdInput: HTMLInputElement;
|
||||
saveMemberId: HTMLButtonElement;
|
||||
memberIdStatus: HTMLParagraphElement;
|
||||
officerViewToggle: HTMLDivElement;
|
||||
officerViewCheckbox: HTMLInputElement;
|
||||
officerContent: HTMLDivElement;
|
||||
resumeList: HTMLTableSectionElement;
|
||||
refreshResumes: HTMLButtonElement;
|
||||
resumeSearch: HTMLInputElement;
|
||||
searchResumes: HTMLButtonElement;
|
||||
profileEditor: HTMLDialogElement;
|
||||
editorName: HTMLInputElement;
|
||||
editorEmail: HTMLInputElement;
|
||||
editorMemberId: HTMLInputElement;
|
||||
editorPoints: HTMLInputElement;
|
||||
editorResume: HTMLInputElement;
|
||||
editorCurrentResume: HTMLParagraphElement;
|
||||
saveProfileButton: HTMLButtonElement;
|
||||
sponsorViewToggle: HTMLDivElement;
|
||||
pdfViewer: HTMLDialogElement;
|
||||
pdfFrame: HTMLIFrameElement;
|
||||
pdfTitle: HTMLHeadingElement;
|
||||
pdfExternalLink: HTMLAnchorElement;
|
||||
}
|
||||
|
||||
export class StoreAuth {
|
||||
private pb: PocketBase;
|
||||
private elements: AuthElements & { loadingSkeleton: HTMLDivElement };
|
||||
private isEditingMemberId: boolean = false;
|
||||
private cachedUsers: any[] = [];
|
||||
private config = config;
|
||||
|
||||
|
@ -137,6 +116,43 @@ export class StoreAuth {
|
|||
};
|
||||
}
|
||||
|
||||
// Public method to handle login
|
||||
public async handleLogin() {
|
||||
try {
|
||||
const authMethods = await this.pb.collection("users").listAuthMethods();
|
||||
const oidcProvider = authMethods.oauth2?.providers?.find(
|
||||
(p: { name: string }) => p.name === this.config.api.oauth2.providerName
|
||||
);
|
||||
|
||||
if (!oidcProvider) {
|
||||
throw new Error("OIDC provider not found");
|
||||
}
|
||||
|
||||
localStorage.setItem("provider", JSON.stringify(oidcProvider));
|
||||
const redirectUrl = window.location.origin + this.config.api.oauth2.redirectPath;
|
||||
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
|
||||
window.location.href = authUrl;
|
||||
} catch (err) {
|
||||
console.error("Authentication error:", err);
|
||||
this.elements.userEmail.textContent = this.config.ui.messages.auth.loginError;
|
||||
this.elements.userName.textContent = "Error";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to update profile settings
|
||||
public async updateProfileSettings(data: {
|
||||
major?: string;
|
||||
graduation_year?: string | number;
|
||||
}) {
|
||||
const user = this.pb.authStore.model;
|
||||
if (!user?.id) {
|
||||
throw new Error("User ID not found");
|
||||
}
|
||||
|
||||
return await this.pb.collection("users").update(user.id, data);
|
||||
}
|
||||
|
||||
private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } {
|
||||
// Get all required elements
|
||||
const loginButton = document.getElementById("contentLoginButton") as HTMLButtonElement;
|
||||
|
@ -148,35 +164,15 @@ export class StoreAuth {
|
|||
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;
|
||||
const profileEditor = document.getElementById("profileEditor") as HTMLDialogElement;
|
||||
const editorName = document.getElementById("editorName") as HTMLInputElement;
|
||||
const editorEmail = document.getElementById("editorEmail") as HTMLInputElement;
|
||||
const editorMemberId = document.getElementById("editorMemberId") as HTMLInputElement;
|
||||
const editorPoints = document.getElementById("editorPoints") as HTMLInputElement;
|
||||
const editorResume = document.getElementById("editorResume") as HTMLInputElement;
|
||||
const editorCurrentResume = document.getElementById("editorCurrentResume") as HTMLParagraphElement;
|
||||
const saveProfileButton = document.getElementById("saveProfileButton") as HTMLButtonElement;
|
||||
const sponsorViewToggle = document.getElementById("sponsorViewToggle") 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;
|
||||
|
||||
// Add CSS for loading state transitions
|
||||
const style = document.createElement("style");
|
||||
|
@ -201,35 +197,15 @@ export class StoreAuth {
|
|||
memberStatus,
|
||||
lastLogin,
|
||||
storeContent,
|
||||
resumeUpload,
|
||||
resumeName,
|
||||
resumeDownload,
|
||||
deleteResume,
|
||||
uploadStatus,
|
||||
resumeActions,
|
||||
memberIdInput,
|
||||
saveMemberId,
|
||||
memberIdStatus,
|
||||
officerViewToggle,
|
||||
officerViewCheckbox,
|
||||
officerContent,
|
||||
resumeList,
|
||||
refreshResumes,
|
||||
resumeSearch,
|
||||
searchResumes,
|
||||
profileEditor,
|
||||
editorName,
|
||||
editorEmail,
|
||||
editorMemberId,
|
||||
editorPoints,
|
||||
editorResume,
|
||||
editorCurrentResume,
|
||||
saveProfileButton,
|
||||
sponsorViewToggle,
|
||||
pdfViewer,
|
||||
pdfFrame,
|
||||
pdfTitle,
|
||||
pdfExternalLink
|
||||
sponsorViewToggle
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -241,20 +217,6 @@ export class StoreAuth {
|
|||
this.elements.loginButton.addEventListener("click", () => this.handleLogin());
|
||||
this.elements.logoutButton.addEventListener("click", () => this.handleLogout());
|
||||
|
||||
// Resume upload event listener
|
||||
this.elements.resumeUpload.addEventListener("change", (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
this.handleResumeUpload(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Resume delete event listener
|
||||
this.elements.deleteResume.addEventListener("click", () => this.handleResumeDelete());
|
||||
|
||||
// Member ID save event listener
|
||||
this.elements.saveMemberId.addEventListener("click", () => this.handleMemberIdButton());
|
||||
|
||||
// Listen for auth state changes
|
||||
this.pb.authStore.onChange(() => {
|
||||
console.log("Auth state changed. IsValid:", this.pb.authStore.isValid);
|
||||
|
@ -276,11 +238,6 @@ export class StoreAuth {
|
|||
e.preventDefault();
|
||||
this.handleProfileSave();
|
||||
});
|
||||
|
||||
// Add resume view event listener
|
||||
document.addEventListener('viewResume', ((e: CustomEvent) => {
|
||||
this.handleResumeView(e.detail.url, e.detail.fileName);
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
private async updateUI() {
|
||||
|
@ -292,12 +249,6 @@ export class StoreAuth {
|
|||
userEmail,
|
||||
memberStatus,
|
||||
lastLogin,
|
||||
memberIdInput,
|
||||
saveMemberId,
|
||||
resumeUpload,
|
||||
resumeName,
|
||||
resumeDownload,
|
||||
resumeActions,
|
||||
loadingSkeleton,
|
||||
officerViewToggle,
|
||||
sponsorViewToggle,
|
||||
|
@ -343,36 +294,11 @@ export class StoreAuth {
|
|||
memberStatus.classList.add("badge-neutral");
|
||||
}
|
||||
|
||||
// Update member ID input and state
|
||||
memberIdInput.value = user.member_id || "";
|
||||
memberIdInput.disabled = false;
|
||||
saveMemberId.disabled = false;
|
||||
|
||||
// Update last login
|
||||
lastLogin.textContent = user.last_login
|
||||
? new Date(user.last_login).toLocaleString()
|
||||
: this.config.ui.messages.auth.never;
|
||||
|
||||
// Update resume section
|
||||
resumeUpload.disabled = false;
|
||||
if (user.resume && (!Array.isArray(user.resume) || user.resume.length > 0)) {
|
||||
const resumeUrl = user.resume.toString();
|
||||
const fileName = this.getFileNameFromUrl(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";
|
||||
} else {
|
||||
resumeName.textContent = "No resume uploaded";
|
||||
resumeDownload.href = "#";
|
||||
resumeDownload.onclick = null;
|
||||
resumeActions.style.display = "none";
|
||||
}
|
||||
|
||||
// Show/hide view toggles
|
||||
officerViewToggle.style.display = isOfficer ? "block" : "none";
|
||||
sponsorViewToggle.style.display = isSponsor ? "block" : "none";
|
||||
|
@ -395,21 +321,6 @@ export class StoreAuth {
|
|||
memberStatus.classList.add("badge-neutral");
|
||||
lastLogin.textContent = this.config.ui.messages.auth.never;
|
||||
|
||||
// Disable inputs
|
||||
memberIdInput.disabled = true;
|
||||
saveMemberId.disabled = true;
|
||||
resumeUpload.disabled = true;
|
||||
|
||||
// Reset member ID
|
||||
memberIdInput.value = "";
|
||||
this.isEditingMemberId = false;
|
||||
|
||||
// Reset resume section
|
||||
resumeName.textContent = "No resume uploaded";
|
||||
resumeDownload.href = "#";
|
||||
resumeDownload.onclick = null;
|
||||
resumeActions.style.display = "none";
|
||||
|
||||
// Hide view toggles
|
||||
officerViewToggle.style.display = "none";
|
||||
sponsorViewToggle.style.display = "none";
|
||||
|
@ -423,148 +334,18 @@ export class StoreAuth {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleLogin() {
|
||||
try {
|
||||
const authMethods = await this.pb.collection("users").listAuthMethods();
|
||||
const oidcProvider = authMethods.oauth2?.providers?.find(
|
||||
(p: { name: string }) => p.name === this.config.api.oauth2.providerName
|
||||
);
|
||||
|
||||
if (!oidcProvider) {
|
||||
throw new Error("OIDC provider not found");
|
||||
}
|
||||
|
||||
localStorage.setItem("provider", JSON.stringify(oidcProvider));
|
||||
const redirectUrl = window.location.origin + this.config.api.oauth2.redirectPath;
|
||||
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
|
||||
window.location.href = authUrl;
|
||||
} catch (err) {
|
||||
console.error("Authentication error:", err);
|
||||
this.elements.userEmail.textContent = this.config.ui.messages.auth.loginError;
|
||||
this.elements.userName.textContent = "Error";
|
||||
}
|
||||
}
|
||||
|
||||
private handleLogout() {
|
||||
this.pb.authStore.clear();
|
||||
this.cachedUsers = [];
|
||||
this.isEditingMemberId = false;
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private getFileNameFromUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathParts = urlObj.pathname.split("/");
|
||||
return decodeURIComponent(pathParts[pathParts.length - 1]);
|
||||
} catch (e) {
|
||||
return url.split("/").pop() || "Unknown File";
|
||||
}
|
||||
}
|
||||
|
||||
private handleResumeView(url: string, fileName: string) {
|
||||
const { pdfViewer, pdfFrame, pdfTitle, pdfExternalLink } = this.elements;
|
||||
pdfFrame.src = url;
|
||||
pdfTitle.textContent = fileName;
|
||||
pdfExternalLink.href = url;
|
||||
pdfViewer.showModal();
|
||||
}
|
||||
|
||||
private async handleResumeUpload(file: File) {
|
||||
const { uploadStatus } = this.elements;
|
||||
try {
|
||||
const user = this.pb.authStore.model;
|
||||
if (!user?.id) {
|
||||
throw new Error("User ID not found");
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("resume", file);
|
||||
|
||||
await this.pb.collection("users").update(user.id, formData);
|
||||
uploadStatus.textContent = this.config.ui.messages.resume.success;
|
||||
this.updateUI();
|
||||
|
||||
setTimeout(() => {
|
||||
uploadStatus.textContent = "";
|
||||
}, this.config.ui.messages.resume.messageTimeout);
|
||||
} catch (err) {
|
||||
console.error("Resume upload error:", err);
|
||||
uploadStatus.textContent = this.config.ui.messages.resume.error;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleResumeDelete() {
|
||||
const { uploadStatus } = this.elements;
|
||||
try {
|
||||
const user = this.pb.authStore.model;
|
||||
if (!user?.id) {
|
||||
throw new Error("User ID not found");
|
||||
}
|
||||
|
||||
await this.pb.collection("users").update(user.id, {
|
||||
resume: null
|
||||
});
|
||||
|
||||
uploadStatus.textContent = this.config.ui.messages.resume.deleteSuccess;
|
||||
this.updateUI();
|
||||
|
||||
setTimeout(() => {
|
||||
uploadStatus.textContent = "";
|
||||
}, this.config.ui.messages.resume.messageTimeout);
|
||||
} catch (err) {
|
||||
console.error("Resume deletion error:", err);
|
||||
uploadStatus.textContent = this.config.ui.messages.resume.deleteError;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMemberIdButton() {
|
||||
const user = this.pb.authStore.model;
|
||||
if (user?.member_id && !this.isEditingMemberId) {
|
||||
this.isEditingMemberId = true;
|
||||
this.updateUI();
|
||||
} else {
|
||||
await this.handleMemberIdSave();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMemberIdSave() {
|
||||
const { memberIdInput, memberIdStatus } = this.elements;
|
||||
const memberId = memberIdInput.value.trim();
|
||||
|
||||
try {
|
||||
memberIdStatus.textContent = this.config.ui.messages.memberId.saving;
|
||||
|
||||
const user = this.pb.authStore.model;
|
||||
if (!user?.id) {
|
||||
throw new Error("User ID not found");
|
||||
}
|
||||
|
||||
await this.pb.collection("users").update(user.id, {
|
||||
member_id: memberId
|
||||
});
|
||||
|
||||
memberIdStatus.textContent = this.config.ui.messages.memberId.success;
|
||||
this.isEditingMemberId = false;
|
||||
this.updateUI();
|
||||
|
||||
setTimeout(() => {
|
||||
memberIdStatus.textContent = "";
|
||||
}, this.config.ui.messages.memberId.messageTimeout);
|
||||
} catch (err) {
|
||||
console.error("IEEE Member ID save error:", err);
|
||||
memberIdStatus.textContent = this.config.ui.messages.memberId.error;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleProfileSave() {
|
||||
const {
|
||||
profileEditor,
|
||||
editorName,
|
||||
editorEmail,
|
||||
editorMemberId,
|
||||
editorPoints,
|
||||
editorResume,
|
||||
saveProfileButton,
|
||||
} = this.elements;
|
||||
const userId = saveProfileButton.dataset.userId;
|
||||
|
@ -575,23 +356,11 @@ export class StoreAuth {
|
|||
}
|
||||
|
||||
try {
|
||||
// First get the current user data to check existing resume
|
||||
const currentUser = await this.pb.collection("users").getOne(userId);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("name", editorName.value);
|
||||
formData.append("email", editorEmail.value);
|
||||
formData.append("member_id", editorMemberId.value);
|
||||
formData.append("points", editorPoints.value);
|
||||
|
||||
// Only append resume if a new file is selected
|
||||
if (editorResume.files && editorResume.files.length > 0) {
|
||||
formData.append("resume", editorResume.files[0]);
|
||||
} else if (currentUser.resume) {
|
||||
// If no new file but there's an existing resume, keep it
|
||||
formData.append("resume", currentUser.resume);
|
||||
}
|
||||
|
||||
await this.pb.collection("users").update(userId, formData);
|
||||
profileEditor.close();
|
||||
this.updateUI();
|
||||
|
|
|
@ -127,104 +127,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Details -->
|
||||
<div class="space-y-4">
|
||||
<!-- IEEE Member ID -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
IEEE Member ID
|
||||
</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type="text"
|
||||
id="memberIdInput"
|
||||
placeholder="Enter your IEEE Member ID"
|
||||
class="join-item input input-bordered flex-1 disabled:bg-base-200 disabled:border-2 disabled:border-opacity-50 disabled:cursor-not-allowed"
|
||||
disabled
|
||||
/>
|
||||
<button
|
||||
id="saveMemberId"
|
||||
class="join-item btn disabled:bg-base-200 disabled:border-2 disabled:border-opacity-50 disabled:cursor-not-allowed enabled:btn-primary hover:enabled:bg-primary-focus"
|
||||
disabled>Save</button
|
||||
>
|
||||
</div>
|
||||
<label class="label">
|
||||
<span id="memberIdStatus" class="label-text-alt"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Resume Upload -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Resume
|
||||
</span>
|
||||
</label>
|
||||
<div id="resumeSection" class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<p id="resumeName" class="text-sm truncate flex-1">
|
||||
No resume uploaded
|
||||
</p>
|
||||
<div id="resumeActions" class="flex gap-2">
|
||||
<a
|
||||
id="resumeDownload"
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="btn btn-ghost btn-xs">View</a
|
||||
>
|
||||
<button
|
||||
id="deleteResume"
|
||||
class="btn btn-ghost btn-xs text-error"
|
||||
>Delete</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploadSection">
|
||||
<input
|
||||
type="file"
|
||||
id="resumeUpload"
|
||||
accept=".pdf,.doc,.docx"
|
||||
class="file-input file-input-bordered file-input-sm w-full disabled:bg-base-200 disabled:border-2 disabled:border-opacity-50 disabled:cursor-not-allowed"
|
||||
disabled
|
||||
/>
|
||||
<label class="label">
|
||||
<span id="uploadStatus" class="label-text-alt"
|
||||
></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth Buttons -->
|
||||
<div class="pt-4">
|
||||
<button
|
||||
id="contentLoginButton"
|
||||
class="login-button btn btn-primary w-full gap-2"
|
||||
class="login-button btn btn-primary w-full gap-2 hidden"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -241,7 +148,7 @@
|
|||
</button>
|
||||
<button
|
||||
id="contentLogoutButton"
|
||||
class="logout-button btn btn-error w-full gap-2"
|
||||
class="logout-button btn btn-error w-full gap-2 hidden"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -409,15 +316,55 @@
|
|||
// Initialize auth and event check-in
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
try {
|
||||
new StoreAuth();
|
||||
const auth = new StoreAuth();
|
||||
new EventCheckIn();
|
||||
|
||||
// Add login button event listener
|
||||
const loginButtons = document.querySelectorAll(".login-button");
|
||||
loginButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const loadingSkeleton =
|
||||
document.getElementById("loadingSkeleton");
|
||||
const userInfo = document.getElementById("userInfo");
|
||||
|
||||
// Show loading state
|
||||
if (loadingSkeleton)
|
||||
loadingSkeleton.style.display = "block";
|
||||
if (userInfo) userInfo.classList.add("hidden");
|
||||
|
||||
// Call the handleLogin method
|
||||
auth.handleLogin().catch((error) => {
|
||||
console.error("Login error:", error);
|
||||
// Show error message
|
||||
if (loadingSkeleton)
|
||||
loadingSkeleton.style.display = "none";
|
||||
if (userInfo) {
|
||||
const errorMessage = document.createElement("div");
|
||||
errorMessage.className = "alert alert-error";
|
||||
errorMessage.innerHTML = `
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span>Failed to sign in. Please try again.</span>
|
||||
</div>
|
||||
`;
|
||||
userInfo.innerHTML = "";
|
||||
userInfo.appendChild(errorMessage);
|
||||
userInfo.classList.remove("hidden");
|
||||
userInfo.style.opacity = "1";
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add error handling for failed initialization
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
console.error("Profile loading error:", event.reason);
|
||||
const userInfo = document.getElementById("userInfo");
|
||||
const loadingSkeleton =
|
||||
document.getElementById("loadingSkeleton");
|
||||
const memberDetails = document.getElementById("memberDetails");
|
||||
const errorMessage = document.createElement("div");
|
||||
|
||||
errorMessage.className = "alert alert-error";
|
||||
|
@ -432,12 +379,32 @@
|
|||
|
||||
if (loadingSkeleton) loadingSkeleton.style.display = "none";
|
||||
if (userInfo) {
|
||||
userInfo.innerHTML = "";
|
||||
if (memberDetails) memberDetails.style.display = "none";
|
||||
userInfo.appendChild(errorMessage);
|
||||
userInfo.classList.remove("hidden");
|
||||
userInfo.style.opacity = "1";
|
||||
}
|
||||
});
|
||||
|
||||
// Check auth state and update UI accordingly
|
||||
const authState = auth.getAuthState();
|
||||
if (!authState.isValid || !authState.model) {
|
||||
const userInfo = document.getElementById("userInfo");
|
||||
const loadingSkeleton =
|
||||
document.getElementById("loadingSkeleton");
|
||||
const memberDetails = document.getElementById("memberDetails");
|
||||
const contentLoginButton =
|
||||
document.getElementById("contentLoginButton");
|
||||
|
||||
if (loadingSkeleton) loadingSkeleton.style.display = "none";
|
||||
if (memberDetails) memberDetails.style.display = "none";
|
||||
if (contentLoginButton)
|
||||
contentLoginButton.classList.remove("hidden");
|
||||
if (userInfo) {
|
||||
userInfo.classList.remove("hidden");
|
||||
userInfo.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize profile:", error);
|
||||
}
|
||||
|
@ -478,7 +445,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Set member since date (you'll need to modify StoreAuth.ts to include this)
|
||||
// Set member since date
|
||||
if (memberSinceElement) {
|
||||
const created = new Date(); // Replace with actual user creation date
|
||||
memberSinceElement.textContent = created.toLocaleDateString();
|
||||
|
|
172
src/components/profile/UserSettings.astro
Normal file
172
src/components/profile/UserSettings.astro
Normal file
|
@ -0,0 +1,172 @@
|
|||
<div class="space-y-6">
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">Account Settings</h2>
|
||||
|
||||
<!-- IEEE Member ID -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">IEEE Member ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="memberIdInput"
|
||||
placeholder="Enter your IEEE Member ID"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Major -->
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Major</span>
|
||||
</label>
|
||||
<select id="majorSelect" class="select select-bordered w-full">
|
||||
<option disabled selected>Select your major</option>
|
||||
<option value="ECE"
|
||||
>Electrical & Computer Engineering</option
|
||||
>
|
||||
<option value="CSE">Computer Science & Engineering</option>
|
||||
<option value="MAE"
|
||||
>Mechanical & Aerospace Engineering</option
|
||||
>
|
||||
<option value="BE">Bioengineering</option>
|
||||
<option value="SE">Structural Engineering</option>
|
||||
<option value="NANO">Nanoengineering</option>
|
||||
<option value="CENG">Chemical Engineering</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Expected Graduation Year -->
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Expected Graduation Year</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="gradYearInput"
|
||||
min="2024"
|
||||
max="2030"
|
||||
placeholder="Enter graduation year"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Resume -->
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Resume</span>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p id="currentResume" class="text-sm opacity-70">
|
||||
No resume uploaded
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
id="resumeUpload"
|
||||
accept=".pdf,.doc,.docx"
|
||||
class="file-input file-input-bordered file-input-sm w-full"
|
||||
/>
|
||||
<label class="label">
|
||||
<span id="uploadStatus" class="label-text-alt"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex justify-end">
|
||||
<button class="btn btn-primary" id="saveSettings">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { StoreAuth } from "../auth/StoreAuth";
|
||||
const auth = new StoreAuth();
|
||||
|
||||
// Get form elements
|
||||
const memberIdInput = document.getElementById(
|
||||
"memberIdInput"
|
||||
) as HTMLInputElement;
|
||||
const majorSelect = document.getElementById(
|
||||
"majorSelect"
|
||||
) as HTMLSelectElement;
|
||||
const gradYearInput = document.getElementById(
|
||||
"gradYearInput"
|
||||
) as HTMLInputElement;
|
||||
const resumeUpload = document.getElementById(
|
||||
"resumeUpload"
|
||||
) as HTMLInputElement;
|
||||
const currentResume = document.getElementById("currentResume");
|
||||
const uploadStatus = document.getElementById("uploadStatus");
|
||||
const saveSettings = document.getElementById("saveSettings");
|
||||
|
||||
// Load current user data
|
||||
const loadUserData = () => {
|
||||
const authState = auth.getAuthState();
|
||||
if (authState.isValid && authState.model) {
|
||||
const user = authState.model;
|
||||
|
||||
// Set current values
|
||||
if (memberIdInput) memberIdInput.value = user.member_id || "";
|
||||
if (majorSelect) majorSelect.value = user.major || "";
|
||||
if (gradYearInput) gradYearInput.value = user.graduation_year || "";
|
||||
|
||||
// Update resume display
|
||||
if (currentResume && user.resume) {
|
||||
const fileName = user.resume.toString().split("/").pop();
|
||||
currentResume.textContent = fileName || "Resume uploaded";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle resume upload
|
||||
if (resumeUpload && uploadStatus) {
|
||||
resumeUpload.addEventListener("change", async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
uploadStatus.textContent = "Uploading...";
|
||||
try {
|
||||
await auth.handleResumeUpload(file);
|
||||
uploadStatus.textContent = "Resume uploaded successfully";
|
||||
if (currentResume) currentResume.textContent = file.name;
|
||||
} catch (err) {
|
||||
console.error("Resume upload error:", err);
|
||||
uploadStatus.textContent = "Failed to upload resume";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle save settings
|
||||
if (saveSettings) {
|
||||
saveSettings.addEventListener("click", async () => {
|
||||
try {
|
||||
const data = {
|
||||
member_id: memberIdInput?.value,
|
||||
major: majorSelect?.value,
|
||||
graduation_year: gradYearInput?.value,
|
||||
};
|
||||
|
||||
await auth.updateProfileSettings(data);
|
||||
alert("Settings saved successfully");
|
||||
loadUserData(); // Reload the data to show updated values
|
||||
} catch (err) {
|
||||
console.error("Failed to save settings:", err);
|
||||
alert("Failed to save settings");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
loadUserData();
|
||||
|
||||
// Update when auth state changes
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "pocketbase_auth") {
|
||||
loadUserData();
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -3,6 +3,7 @@ import Layout from "../layouts/Layout.astro";
|
|||
import UserProfile from "../components/auth/UserProfile.astro";
|
||||
import DefaultProfileView from "../components/profile/DefaultProfileView.astro";
|
||||
import OfficerProfileView from "../components/profile/OfficerView.astro";
|
||||
import UserSettings from "../components/profile/UserSettings.astro";
|
||||
const title = "User Profile";
|
||||
---
|
||||
|
||||
|
@ -56,6 +57,50 @@ const title = "User Profile";
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Not Authenticated State -->
|
||||
<div id="notAuthenticatedState" class="hidden w-full">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 text-base-content/30"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="card-title text-2xl mb-2">
|
||||
Sign in to Access Your Profile
|
||||
</h2>
|
||||
<p class="text-base-content/70 mb-6">
|
||||
Please sign in with your IEEE UCSD account to view
|
||||
and manage your profile.
|
||||
</p>
|
||||
<button
|
||||
class="login-button btn btn-primary btn-lg gap-2"
|
||||
>
|
||||
<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="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Sign in with IEEEUCSD SSO
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div
|
||||
id="mainContent"
|
||||
|
@ -217,6 +262,9 @@ const title = "User Profile";
|
|||
<div id="defaultView">
|
||||
<DefaultProfileView />
|
||||
</div>
|
||||
<div id="settingsView" class="hidden">
|
||||
<UserSettings />
|
||||
</div>
|
||||
<div id="officerView" class="hidden">
|
||||
<OfficerProfileView />
|
||||
</div>
|
||||
|
@ -233,6 +281,9 @@ const title = "User Profile";
|
|||
// Initialize page state
|
||||
const pageLoadingState = document.getElementById("pageLoadingState");
|
||||
const pageErrorState = document.getElementById("pageErrorState");
|
||||
const notAuthenticatedState = document.getElementById(
|
||||
"notAuthenticatedState"
|
||||
);
|
||||
const mainContent = document.getElementById("mainContent");
|
||||
const tabs = document.querySelectorAll(".tab");
|
||||
const defaultView = document.getElementById("defaultView");
|
||||
|
@ -273,12 +324,18 @@ const title = "User Profile";
|
|||
// Show loading state
|
||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.add("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.add("hidden");
|
||||
if (mainContent) mainContent.classList.add("hidden");
|
||||
|
||||
// Check auth state
|
||||
const authState = auth.getAuthState();
|
||||
if (!authState.isValid || !authState.model) {
|
||||
throw new Error("User not authenticated");
|
||||
// Show not authenticated state
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = authState.model;
|
||||
|
@ -350,12 +407,26 @@ const title = "User Profile";
|
|||
|
||||
// Update content visibility
|
||||
const tabId = (tab as HTMLElement).dataset.tab;
|
||||
if (tabId === "events") {
|
||||
defaultView!.style.display = "block";
|
||||
officerView!.style.display = "none";
|
||||
} else if (tabId === "officer") {
|
||||
defaultView!.style.display = "none";
|
||||
officerView!.style.display = "block";
|
||||
const defaultView = document.getElementById("defaultView");
|
||||
const settingsView = document.getElementById("settingsView");
|
||||
const officerView = document.getElementById("officerView");
|
||||
|
||||
if (defaultView && settingsView && officerView) {
|
||||
defaultView.classList.add("hidden");
|
||||
settingsView.classList.add("hidden");
|
||||
officerView.classList.add("hidden");
|
||||
|
||||
switch (tabId) {
|
||||
case "events":
|
||||
defaultView.classList.remove("hidden");
|
||||
break;
|
||||
case "settings":
|
||||
settingsView.classList.remove("hidden");
|
||||
break;
|
||||
case "officer":
|
||||
officerView.classList.remove("hidden");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -389,6 +460,25 @@ const title = "User Profile";
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add login button event listener
|
||||
const loginButtons = document.querySelectorAll(".login-button");
|
||||
loginButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
// Show loading state while authentication is in progress
|
||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.add("hidden");
|
||||
|
||||
// Call the handleLogin method from StoreAuth
|
||||
auth.handleLogin().catch((error) => {
|
||||
console.error("Login error:", error);
|
||||
// Show error state if login fails
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
Loading…
Reference in a new issue