From 72cb6ecb497c4849f0363abfbc2d5cd7a7b168e9 Mon Sep 17 00:00:00 2001 From: chark1es Date: Fri, 24 Jan 2025 04:00:29 -0800 Subject: [PATCH 001/224] Add online store --- astro.config.mjs | 8 +- bun.lock | 12 + package.json | 2 + src/components/auth/RedirectHandler.ts | 85 ++++++ src/components/auth/StoreAuth.ts | 382 +++++++++++++++++++++++++ src/components/auth/UserProfile.astro | 193 +++++++++++++ src/components/store/StoreItem.astro | 37 +++ src/layouts/Layout.astro | 7 +- src/pages/oauth2-redirect.astro | 20 ++ src/pages/online-store.astro | 51 ++++ tailwind.config.mjs | 7 +- 11 files changed, 797 insertions(+), 7 deletions(-) create mode 100644 src/components/auth/RedirectHandler.ts create mode 100644 src/components/auth/StoreAuth.ts create mode 100644 src/components/auth/UserProfile.astro create mode 100644 src/components/store/StoreItem.astro create mode 100644 src/pages/oauth2-redirect.astro create mode 100644 src/pages/online-store.astro diff --git a/astro.config.mjs b/astro.config.mjs index 17e7024..8f74173 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -15,9 +15,9 @@ import icon from "astro-icon"; // https://astro.build/config export default defineConfig({ - integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()], + integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()], - adapter: node({ - mode: "standalone", - }), + adapter: node({ + mode: "standalone", + }), }); diff --git a/bun.lock b/bun.lock index f94e971..282fbe1 100644 --- a/bun.lock +++ b/bun.lock @@ -14,12 +14,14 @@ "astro-icon": "^1.1.4", "motion": "^11.15.0", "next": "^15.1.2", + "pocketbase": "^0.25.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.4.0", "tailwindcss": "^3.4.16", }, "devDependencies": { + "daisyui": "^4.12.23", "prettier": "^3.4.2", "prettier-plugin-astro": "^0.14.1", "tailwindcss-animated": "^1.1.2", @@ -502,6 +504,8 @@ "css-selector-parser": ["css-selector-parser@3.0.5", "", {}, "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g=="], + "css-selector-tokenizer": ["css-selector-tokenizer@0.8.0", "", { "dependencies": { "cssesc": "^3.0.0", "fastparse": "^1.1.2" } }, "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg=="], + "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], @@ -512,6 +516,10 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "culori": ["culori@3.3.0", "", {}, "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ=="], + + "daisyui": ["daisyui@4.12.23", "", { "dependencies": { "css-selector-tokenizer": "^0.8", "culori": "^3", "picocolors": "^1", "postcss-js": "^4" } }, "sha512-EM38duvxutJ5PD65lO/AFMpcw+9qEy6XAZrTpzp7WyaPeO/l+F/Qiq0ECHHmFNcFXh5aVoALY4MGrrxtCiaQCQ=="], + "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="], @@ -616,6 +624,8 @@ "fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="], + "fastparse": ["fastparse@1.1.2", "", {}, "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ=="], + "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], @@ -1034,6 +1044,8 @@ "pkg-types": ["pkg-types@1.2.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.2", "pathe": "^1.1.2" } }, "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw=="], + "pocketbase": ["pocketbase@0.25.1", "", {}, "sha512-2IH0KLI/qMNR/E17C7BGWX2FxW7Tead+igLHOWZ45P56v/NyVT18Jnmddeft+3qWWGL1Hog2F8bc4orWV/+Fcg=="], + "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], diff --git a/package.json b/package.json index 523d7b7..45d74f2 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "astro-icon": "^1.1.4", "motion": "^11.15.0", "next": "^15.1.2", + "pocketbase": "^0.25.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.4.0", "tailwindcss": "^3.4.16" }, "devDependencies": { + "daisyui": "^4.12.23", "prettier": "^3.4.2", "prettier-plugin-astro": "^0.14.1", "tailwindcss-animated": "^1.1.2", diff --git a/src/components/auth/RedirectHandler.ts b/src/components/auth/RedirectHandler.ts new file mode 100644 index 0000000..4482f4e --- /dev/null +++ b/src/components/auth/RedirectHandler.ts @@ -0,0 +1,85 @@ +import PocketBase from "pocketbase"; + +export class RedirectHandler { + private pb: PocketBase; + private contentEl: HTMLElement; + private params: URLSearchParams; + private provider: any; + + constructor() { + this.pb = new PocketBase("https://pocketbase.ieeeucsd.org"); + this.contentEl = this.getContentElement(); + this.params = new URLSearchParams(window.location.search); + this.provider = this.getStoredProvider(); + this.handleRedirect(); + } + + private getContentElement(): HTMLElement { + const contentEl = document.getElementById("content"); + if (!contentEl) { + throw new Error("Content element not found"); + } + return contentEl; + } + + private getStoredProvider() { + return JSON.parse(localStorage.getItem("provider") || "{}"); + } + + private showError(message: string) { + this.contentEl.innerHTML = `

${message}

`; + } + + private async handleRedirect() { + const code = this.params.get("code"); + const state = this.params.get("state"); + + if (!code) { + this.showError("No authorization code found in URL."); + return; + } + + if (state !== this.provider.state) { + this.showError("Invalid state parameter."); + return; + } + + try { + const authData = await this.pb.collection("users").authWithOAuth2Code( + "oidc", + code, + this.provider.codeVerifier, + window.location.origin + "/oauth2-redirect", + { emailVisibility: false } + ); + + console.log("Auth successful:", authData); + this.contentEl.innerHTML = ` +

Authentication Successful!

+

Redirecting to store...

+
+
+
+ `; + + try { + // Update last login before redirecting + await this.pb.collection("users").update(authData.record.id, { + last_login: new Date().toISOString() + }); + + // Clean up and redirect + localStorage.removeItem("provider"); + window.location.href = "/online-store"; + } catch (err) { + console.error("Failed to update last login:", err); + // Still redirect even if last_login update fails + localStorage.removeItem("provider"); + window.location.href = "/online-store"; + } + } catch (err: any) { + console.error("Auth error:", err); + this.showError(`Failed to complete authentication: ${err.message}`); + } + } +} \ No newline at end of file diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts new file mode 100644 index 0000000..172a7f8 --- /dev/null +++ b/src/components/auth/StoreAuth.ts @@ -0,0 +1,382 @@ +import PocketBase from "pocketbase"; + +interface AuthElements { + loginButton: HTMLButtonElement; + logoutButton: HTMLButtonElement; + userInfo: HTMLDivElement; + userName: HTMLParagraphElement; + userEmail: HTMLParagraphElement; + memberStatus: HTMLDivElement; + lastLogin: HTMLParagraphElement; + storeContent: HTMLDivElement; + resumeUpload: HTMLInputElement; + resumeName: HTMLParagraphElement; + resumeDownload: HTMLAnchorElement; + deleteResume: HTMLButtonElement; + uploadStatus: HTMLParagraphElement; + resumeActions: HTMLDivElement; + memberIdInput: HTMLInputElement; + saveMemberId: HTMLButtonElement; + memberIdStatus: HTMLParagraphElement; +} + +export class StoreAuth { + private pb: PocketBase; + private elements: AuthElements & { loadingSkeleton: HTMLDivElement }; + private isEditingMemberId: boolean = false; + + constructor() { + this.pb = new PocketBase("https://pocketbase.ieeeucsd.org"); + this.elements = this.getElements(); + this.init(); + } + + private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } { + // Fun typescript fixes + 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; + + // Add CSS for loading state transitions + const style = document.createElement('style'); + style.textContent = ` + .loading-state { + opacity: 0.5; + transition: opacity 0.3s ease-in-out; + } + .content-ready { + opacity: 1; + } + `; + 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; + + if (!loginButton || !logoutButton || !userInfo || !storeContent || !userName || !userEmail || + !memberStatus || !lastLogin || !resumeUpload || !resumeName || !loadingSkeleton || + !resumeDownload || !deleteResume || !uploadStatus || !resumeActions || + !memberIdInput || !saveMemberId || !memberIdStatus) { + 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 + }; + } + + private updateMemberIdState() { + const { memberIdInput, saveMemberId } = this.elements; + const user = this.pb.authStore.model; + + if (user?.member_id && !this.isEditingMemberId) { + // Has member ID and not editing - show update button and disable input + memberIdInput.disabled = true; + memberIdInput.value = user.member_id; + saveMemberId.textContent = "Update"; + saveMemberId.classList.remove("btn-primary"); + saveMemberId.classList.add("btn-ghost"); + } else { + // No member ID or editing - show save button and enable input + memberIdInput.disabled = false; + saveMemberId.textContent = "Save"; + saveMemberId.classList.remove("btn-ghost"); + saveMemberId.classList.add("btn-primary"); + } + } + + private async updateUI() { + const { loginButton, logoutButton, userInfo, userName, userEmail, memberStatus, + lastLogin, storeContent, resumeName, resumeDownload, resumeActions, + memberIdInput, saveMemberId, resumeUpload, loadingSkeleton } = this.elements; + + // Hide buttons initially + loginButton.style.display = 'none'; + logoutButton.style.display = 'none'; + + if (this.pb.authStore.isValid && this.pb.authStore.model) { + // Update all the user information first + const user = this.pb.authStore.model; + userName.textContent = user.name || "Name not provided"; + userEmail.textContent = user.email || "Email not available"; + + // Update member status + if (user.verified) { + // 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"; + + await this.pb.collection("users").update(user.id, { + member_type: newMemberType + }); + + user.member_type = newMemberType; + } catch (err) { + console.error("Failed to update member type:", err); + } + } + + memberStatus.textContent = user.member_type || "Regular Member"; + 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") { + memberStatus.classList.add("badge-warning"); // Red for administrators + } else if (user.member_type === "IEEE Officer") { + memberStatus.classList.add("badge-info"); // Blue for officers + } else { + memberStatus.classList.add("badge-neutral"); // Yellow for regular members + } + } else { + memberStatus.textContent = "Not Verified"; + memberStatus.classList.remove("badge-info", "badge-warning", "badge-success", "badge-error"); + memberStatus.classList.add("badge-neutral"); + } + + // Update member ID input and state + memberIdInput.value = user.member_id || ""; + this.updateMemberIdState(); + + // Update last login + 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)) { + const resumeUrl = user.resume.toString(); + resumeName.textContent = this.getFileNameFromUrl(resumeUrl); + resumeDownload.href = this.pb.files.getURL(user, resumeUrl); + resumeActions.style.display = 'flex'; + } else { + resumeName.textContent = "No resume uploaded"; + resumeDownload.href = "#"; + resumeActions.style.display = 'none'; + } + + // After everything is updated, show the content + loadingSkeleton.style.display = 'none'; + userInfo.classList.remove('hidden'); + // Use a small delay to ensure the transition works + setTimeout(() => { + userInfo.style.opacity = '1'; + }, 50); + + 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.add("badge-neutral"); + lastLogin.textContent = "Never"; + + // Reset member ID + memberIdInput.value = ""; + memberIdInput.disabled = true; + this.isEditingMemberId = false; + this.updateMemberIdState(); + + // Reset resume section + resumeName.textContent = "No resume uploaded"; + resumeDownload.href = "#"; + resumeActions.style.display = 'none'; + + // After everything is updated, show the content + loadingSkeleton.style.display = 'none'; + userInfo.classList.remove('hidden'); + // Use a small delay to ensure the transition works + setTimeout(() => { + userInfo.style.opacity = '1'; + }, 50); + + loginButton.style.display = 'block'; + } + } + + private getFileNameFromUrl(url: string): string { + const parts = url.split("/"); + return parts[parts.length - 1]; + } + + private async handleMemberIdButton() { + const user = this.pb.authStore.model; + + if (user?.member_id && !this.isEditingMemberId) { + // If we have a member ID and we're not editing, switch to edit mode + this.isEditingMemberId = true; + this.updateMemberIdState(); + } else { + // If we're editing or don't have a member ID, try to save + await this.handleMemberIdSave(); + } + } + + private async handleMemberIdSave() { + const { memberIdInput, memberIdStatus } = this.elements; + const memberId = memberIdInput.value.trim(); + + try { + memberIdStatus.textContent = "Saving member ID..."; + + 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 = "Member ID saved successfully!"; + this.isEditingMemberId = false; + this.updateUI(); + + // Clear the status message after a delay + setTimeout(() => { + memberIdStatus.textContent = ""; + }, 3000); + } catch (err: any) { + console.error("Member ID save error:", err); + memberIdStatus.textContent = "Failed to save member ID. Please try again."; + } + } + + private async handleResumeUpload(file: File) { + const { uploadStatus } = this.elements; + + try { + uploadStatus.textContent = "Uploading resume..."; + + const formData = new FormData(); + formData.append("resume", file); + + const user = this.pb.authStore.model; + if (!user?.id) { + throw new Error("User ID not found"); + } + + await this.pb.collection("users").update(user.id, formData); + + uploadStatus.textContent = "Resume uploaded successfully!"; + this.updateUI(); + + // Clear the file input + this.elements.resumeUpload.value = ""; + + // Clear the status message after a delay + setTimeout(() => { + uploadStatus.textContent = ""; + }, 3000); + } catch (err: any) { + console.error("Resume upload error:", err); + uploadStatus.textContent = "Failed to upload resume. Please try again."; + } + } + + private async handleResumeDelete() { + const { uploadStatus } = this.elements; + + try { + uploadStatus.textContent = "Deleting resume..."; + + 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 = "Resume deleted successfully!"; + this.updateUI(); + + // Clear the status message after a delay + setTimeout(() => { + uploadStatus.textContent = ""; + }, 3000); + } catch (err: any) { + console.error("Resume deletion error:", err); + uploadStatus.textContent = "Failed to delete resume. Please try again."; + } + } + + private async handleLogin() { + console.log("Starting OAuth2 authentication..."); + try { + const authMethods = await this.pb.collection("users").listAuthMethods(); + const oidcProvider = authMethods.oauth2?.providers?.find( + (p: { name: string }) => p.name === "oidc" + ); + + if (!oidcProvider) { + throw new Error("OIDC provider not found"); + } + + // Store provider info for the redirect page + localStorage.setItem("provider", JSON.stringify(oidcProvider)); + + // Redirect to the authorization URL + const redirectUrl = window.location.origin + "/oauth2-redirect"; + const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl); + window.location.href = authUrl; + } catch (err: any) { + console.error("Authentication error:", err); + this.elements.userEmail.textContent = "Failed to start authentication"; + this.elements.userName.textContent = "Error"; + } + } + + private handleLogout() { + this.pb.authStore.clear(); + this.updateUI(); + } + + 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()); + + // 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(async (token) => { + console.log("Auth state changed. IsValid:", this.pb.authStore.isValid); + this.updateUI(); + }); + } +} \ No newline at end of file diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro new file mode 100644 index 0000000..69544ff --- /dev/null +++ b/src/components/auth/UserProfile.astro @@ -0,0 +1,193 @@ +
+ +
+
+
+ +

+
+ +
+
+ +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + + +
+ + diff --git a/src/components/store/StoreItem.astro b/src/components/store/StoreItem.astro new file mode 100644 index 0000000..cb43edc --- /dev/null +++ b/src/components/store/StoreItem.astro @@ -0,0 +1,37 @@ +--- +interface Props { + name: string; + description: string; + price: number; + imageUrl: string; +} + +const { + name, + description, + price, + imageUrl = "https://placehold.co/400x300", +} = Astro.props; +--- + +
+
+
+
+ {name} +
+
+
+

{name}

+

{description}

+
+ ${price.toFixed(2)} + +
+
+
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 9c9a4ac..b8e56c6 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -14,7 +14,9 @@ import InView from "../components/core/InView.astro"; Astro Basics - +
@@ -23,6 +25,9 @@ import InView from "../components/core/InView.astro"; diff --git a/src/components/store/StoreItem.astro b/src/components/store/StoreItem.astro index cb43edc..6cf9941 100644 --- a/src/components/store/StoreItem.astro +++ b/src/components/store/StoreItem.astro @@ -1,37 +1,37 @@ --- interface Props { - name: string; - description: string; - price: number; - imageUrl: string; + name: string; + description: string; + price: number; + imageUrl: string; } const { - name, - description, - price, - imageUrl = "https://placehold.co/400x300", + name, + description, + price, + imageUrl = "https://placehold.co/400x300", } = Astro.props; ---
-
-
-
- {name} -
-
-
-

{name}

-

{description}

-
- ${price.toFixed(2)} - -
+
+
+
+ {name}
+
+
+

{name}

+

{description}

+
+ ${price.toFixed(2)} + +
+
diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro index 35e637c..58e46f0 100644 --- a/src/pages/online-store.astro +++ b/src/pages/online-store.astro @@ -6,46 +6,118 @@ const title = "IEEE Store"; --- -
-

IEEE UCSD Store

-
- -
- -
+
+

IEEE UCSD Store

+
+ +
+ +
- -
-
- - - - -
-
+ +
+
+ + + +
-
+ + +
+
+
From 36840716734ff5a5ebc26483966430dcd17e371d Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 27 Jan 2025 16:46:23 -0800 Subject: [PATCH 006/224] allows editing profiles --- src/components/auth/StoreAuth.ts | 394 +++++++++++++------------- src/components/auth/UserProfile.astro | 76 +++++ src/pages/online-store.astro | 2 +- 3 files changed, 280 insertions(+), 192 deletions(-) diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts index 2570132..21e43ad 100644 --- a/src/components/auth/StoreAuth.ts +++ b/src/components/auth/StoreAuth.ts @@ -25,6 +25,14 @@ interface AuthElements { refreshResumes: HTMLButtonElement; resumeSearch: HTMLInputElement; searchResumes: HTMLButtonElement; + profileEditor: HTMLDialogElement; + editorName: HTMLInputElement; + editorEmail: HTMLInputElement; + editorMemberId: HTMLInputElement; + editorPoints: HTMLInputElement; + editorResume: HTMLInputElement; + editorCurrentResume: HTMLParagraphElement; + saveProfileButton: HTMLButtonElement; } export class StoreAuth { @@ -128,6 +136,31 @@ export class StoreAuth { "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; + if ( !loginButton || !logoutButton || @@ -153,7 +186,15 @@ export class StoreAuth { !resumeList || !refreshResumes || !resumeSearch || - !searchResumes + !searchResumes || + !profileEditor || + !editorName || + !editorEmail || + !editorMemberId || + !editorPoints || + !editorResume || + !editorCurrentResume || + !saveProfileButton ) { throw new Error("Required DOM elements not found"); } @@ -184,6 +225,14 @@ export class StoreAuth { refreshResumes, resumeSearch, searchResumes, + profileEditor, + editorName, + editorEmail, + editorMemberId, + editorPoints, + editorResume, + editorCurrentResume, + saveProfileButton, }; } @@ -374,8 +423,13 @@ export class StoreAuth { } private getFileNameFromUrl(url: string): string { - const parts = url.split("/"); - return parts[parts.length - 1]; + 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 async handleMemberIdButton() { @@ -436,6 +490,15 @@ export class StoreAuth { throw new Error("User ID not found"); } + // Get current user data first + const currentUser = await this.pb.collection("users").getOne(user.id); + + // Keep existing data + formData.append("name", currentUser.name || ""); + formData.append("email", currentUser.email || ""); + formData.append("member_id", currentUser.member_id || ""); + formData.append("points", currentUser.points?.toString() || "0"); + await this.pb.collection("users").update(user.id, formData); uploadStatus.textContent = "Resume uploaded successfully!"; @@ -515,7 +578,7 @@ export class StoreAuth { private async fetchUserResumes(searchQuery: string = "") { try { - let filter = 'resume != ""'; + let filter = ""; // Remove the resume filter to show all users if (searchQuery) { const terms = searchQuery .toLowerCase() @@ -528,16 +591,20 @@ export class StoreAuth { `(name ?~ "${term}" || email ?~ "${term}" || member_id ?~ "${term}")`, ) .join(" && "); - filter += ` && (${searchConditions})`; + filter = searchConditions; // Only apply search conditions } } const records = await this.pb.collection("users").getList(1, 50, { filter, sort: "-updated", - fields: "id,name,email,member_id,resume,updated,points", + fields: + "id,name,email,member_id,resume,points,collectionId,collectionName", + expand: "resume", }); + console.log("Fetched records:", records.items); // Debug log + const { resumeList } = this.elements; const fragment = document.createDocumentFragment(); @@ -545,16 +612,19 @@ export class StoreAuth { const row = document.createElement("tr"); row.innerHTML = ` - ${searchQuery ? "No users found matching your search." : "No resumes uploaded yet."} + ${searchQuery ? "No users found matching your search." : "No users found."} `; 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; + const resumeUrl = + user.resume && user.resume !== "" + ? this.pb.files.getURL(user, user.resume.toString()) + : null; + + console.log("User resume:", user.resume, "Resume URL:", resumeUrl); // Debug log row.innerHTML = ` @@ -563,40 +633,7 @@ export class StoreAuth {
${user.name || "N/A"}
${user.email || "N/A"}
ID: ${user.member_id || "N/A"}
-
-
- Points: -
- ${user.points || 0} - -
- -
-
+
Points: ${user.points || 0}
${ resumeUrl @@ -607,9 +644,12 @@ export class StoreAuth { ` : 'No resume' } - - ${new Date(user.updated).toLocaleDateString()} - +
@@ -618,37 +658,7 @@ export class StoreAuth { ${user.email || "N/A"} ${user.member_id || "N/A"} - -
- ${user.points || 0} - -
- - + ${user.points || 0} ${ resumeUrl @@ -660,7 +670,14 @@ export class StoreAuth { : 'No resume' } - ${new Date(user.updated).toLocaleDateString()} + + + `; fragment.appendChild(row); @@ -670,8 +687,16 @@ export class StoreAuth { resumeList.innerHTML = ""; resumeList.appendChild(fragment); - // Setup event listeners for the points editing functionality - this.setupPointsEventListeners(); + // Setup edit profile event listeners + const editButtons = resumeList.querySelectorAll(".edit-profile"); + editButtons.forEach((button) => { + button.addEventListener("click", () => { + const userId = (button as HTMLButtonElement).dataset.userId; + if (userId) { + this.handleProfileEdit(userId); + } + }); + }); } catch (err) { console.error("Failed to fetch user resumes:", err); const { resumeList } = this.elements; @@ -685,131 +710,102 @@ export class StoreAuth { } } - private async updateUserPoints(userId: string, points: number) { + private async handleProfileEdit(userId: string) { try { - await this.pb.collection("users").update(userId, { - points: points, - }); + const user = await this.pb.collection("users").getOne(userId); + const { + profileEditor, + editorName, + editorEmail, + editorMemberId, + editorPoints, + editorCurrentResume, + saveProfileButton, + } = this.elements; - // 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"); + // Populate the form + editorName.value = user.name || ""; + editorEmail.value = user.email || ""; + editorMemberId.value = user.member_id || ""; + editorPoints.value = user.points?.toString() || "0"; + + // Update resume display + if (user.resume) { + const resumeUrl = this.pb.files.getURL(user, user.resume.toString()); + const fileName = this.getFileNameFromUrl(resumeUrl); + editorCurrentResume.textContent = `Current resume: ${fileName}`; + editorCurrentResume.classList.remove("opacity-70"); + } else { + editorCurrentResume.textContent = "No resume uploaded"; + editorCurrentResume.classList.add("opacity-70"); } + + // Store the user ID for saving + saveProfileButton.dataset.userId = userId; + + // Show the dialog + profileEditor.showModal(); } catch (err) { - console.error("Failed to update points:", err); + console.error("Failed to load user for editing:", err); } } - private setupPointsEventListeners() { - // Use event delegation for all points-related actions - const { resumeList } = this.elements; + private async handleProfileSave() { + const { + profileEditor, + editorName, + editorEmail, + editorMemberId, + editorPoints, + editorResume, + saveProfileButton, + } = this.elements; + const userId = saveProfileButton.dataset.userId; - 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; + if (!userId) { + console.error("No user ID found for saving"); + return; + } - const userId = button.dataset.userId; - console.log("User ID:", userId); - if (!userId) return; + try { + // First get the current user data to check existing resume + const currentUser = await this.pb.collection("users").getOne(userId); - // 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 formData = new FormData(); + formData.append("name", editorName.value); + formData.append("email", editorEmail.value); + formData.append("member_id", editorMemberId.value); + formData.append("points", editorPoints.value); - 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"); - } + // 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); } - // Handle confirm button click - if (button.classList.contains("confirm-points")) { - const row = button.closest("tr"); - if (!row) return; + // Log the form data for debugging + console.log("Form data being sent:", { + name: editorName.value, + email: editorEmail.value, + member_id: editorMemberId.value, + points: editorPoints.value, + hasNewResume: editorResume.files && editorResume.files.length > 0, + hasExistingResume: !!currentUser.resume, + }); - 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 updatedUser = await this.pb + .collection("users") + .update(userId, formData); + console.log("Update response:", updatedUser); - 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"); - } - } - }); + // Close the dialog and refresh the table + profileEditor.close(); + this.fetchUserResumes(); + } catch (err) { + console.error("Failed to save user profile:", err); + } } private init() { @@ -883,5 +879,21 @@ export class StoreAuth { console.log("Auth state changed. IsValid:", this.pb.authStore.isValid); this.updateUI(); }); + + // Profile editor event listeners + const { profileEditor, saveProfileButton } = this.elements; + + // Close dialog when clicking outside + profileEditor.addEventListener("click", (e) => { + if (e.target === profileEditor) { + profileEditor.close(); + } + }); + + // Save profile button + saveProfileButton.addEventListener("click", (e) => { + e.preventDefault(); + this.handleProfileSave(); + }); } } diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro index b129a15..3394574 100644 --- a/src/components/auth/UserProfile.astro +++ b/src/components/auth/UserProfile.astro @@ -171,6 +171,76 @@ + + + + + + diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro index 58e46f0..68dc235 100644 --- a/src/pages/online-store.astro +++ b/src/pages/online-store.astro @@ -103,7 +103,7 @@ const title = "IEEE Store"; Member ID Points Resume - Last Updated + Actions From dee0a65dbef0fcfe217a6d5cc2b0f222260e8ddd Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 27 Jan 2025 17:00:22 -0800 Subject: [PATCH 007/224] fix mobile view --- src/components/auth/StoreAuth.ts | 4 +-- src/pages/online-store.astro | 47 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts index 21e43ad..a104c73 100644 --- a/src/components/auth/StoreAuth.ts +++ b/src/components/auth/StoreAuth.ts @@ -857,9 +857,7 @@ export class StoreAuth { // 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 storeItemsContainer = document.getElementById("storeItemsGrid"); const { officerContent } = this.elements; if (storeItemsContainer) { diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro index 68dc235..42dc43c 100644 --- a/src/pages/online-store.astro +++ b/src/pages/online-store.astro @@ -10,33 +10,34 @@ const title = "IEEE Store";

IEEE UCSD Store

-
+
-
-
- - - - +
+
+
+
+
+

+ Store Coming Soon! +

+
+

+ Our store is currently under development. Check back later + for IEEE UCSD merchandise! In the meantime, please make sure + your profile is up to date with your IEEE Member ID and + resume. +

+
+
+
+
@@ -221,75 +121,4 @@ const title = "IEEE Store"; From 50960f693928c593514cabeeacf4e3769f8a0faa Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 28 Jan 2025 02:39:55 -0800 Subject: [PATCH 010/224] disable button when not logged in --- src/components/auth/StoreAuth.ts | 16 +- src/components/auth/UserProfile.astro | 513 ++++++++++++++------------ 2 files changed, 290 insertions(+), 239 deletions(-) diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts index a104c73..6fab42e 100644 --- a/src/components/auth/StoreAuth.ts +++ b/src/components/auth/StoreAuth.ts @@ -284,9 +284,17 @@ export class StoreAuth { if (this.pb.authStore.isValid && this.pb.authStore.model) { // Update all the user information first const user = this.pb.authStore.model; + userName.textContent = user.name || "Name not provided"; userEmail.textContent = user.email || "Email not available"; + // Enable member ID input and save button + memberIdInput.disabled = false; + saveMemberId.disabled = false; + + // Enable resume upload + resumeUpload.disabled = false; + // Update member status if (user.verified) { // Check and update member_type if not set @@ -398,9 +406,15 @@ export class StoreAuth { memberStatus.classList.add("badge-neutral"); lastLogin.textContent = "Never"; + // Disable member ID input and save button + memberIdInput.disabled = true; + saveMemberId.disabled = true; + + // Disable resume upload + resumeUpload.disabled = true; + // Reset member ID memberIdInput.value = ""; - memberIdInput.disabled = true; this.isEditingMemberId = false; this.updateMemberIdState(); diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro index 3394574..c1e350d 100644 --- a/src/components/auth/UserProfile.astro +++ b/src/components/auth/UserProfile.astro @@ -1,257 +1,294 @@
- -
-
-
- -

-
- -
-
- -
-
-
-
-
- - -
-
-
-
-
- - -
-
-
-
-
- - -
-
-
-
-
+ +
+
+
+ +

-
-
+ +
+
+ +
+
+
+
+
- -
-
-
-
-
+ +
+
+
+
+
- -
-
-
-
-
-
-
-
-
-
-
+ +
+
+
+
+
- -
-
-
-
-
-
-
+ +
+
+
+
+
+
+
+
- -
- - From e0b82cdf73d885d98c9edfa34f6db784fd50ccdf Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 28 Jan 2025 02:57:52 -0800 Subject: [PATCH 011/224] added sponsor view, fix logout session --- src/components/auth/StoreAuth.ts | 262 ++++++++++++++++---------- src/components/auth/UserProfile.astro | 13 +- 2 files changed, 174 insertions(+), 101 deletions(-) diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts index 6fab42e..8511a8d 100644 --- a/src/components/auth/StoreAuth.ts +++ b/src/components/auth/StoreAuth.ts @@ -33,12 +33,14 @@ interface AuthElements { editorResume: HTMLInputElement; editorCurrentResume: HTMLParagraphElement; saveProfileButton: HTMLButtonElement; + sponsorViewToggle: HTMLDivElement; } export class StoreAuth { private pb: PocketBase; private elements: AuthElements & { loadingSkeleton: HTMLDivElement }; private isEditingMemberId: boolean = false; + private cachedUsers: any[] = []; // Store users data constructor() { this.pb = new PocketBase("https://pocketbase.ieeeucsd.org"); @@ -161,6 +163,10 @@ export class StoreAuth { "saveProfileButton", ) as HTMLButtonElement; + const sponsorViewToggle = document.getElementById( + "sponsorViewToggle", + ) as HTMLDivElement; + if ( !loginButton || !logoutButton || @@ -194,7 +200,8 @@ export class StoreAuth { !editorPoints || !editorResume || !editorCurrentResume || - !saveProfileButton + !saveProfileButton || + !sponsorViewToggle ) { throw new Error("Required DOM elements not found"); } @@ -233,6 +240,7 @@ export class StoreAuth { editorResume, editorCurrentResume, saveProfileButton, + sponsorViewToggle, }; } @@ -245,14 +253,14 @@ export class StoreAuth { memberIdInput.disabled = true; memberIdInput.value = user.member_id; saveMemberId.textContent = "Update"; - saveMemberId.classList.remove("btn-primary"); - saveMemberId.classList.add("btn-ghost"); + saveMemberId.classList.remove("enabled:btn-primary"); + saveMemberId.classList.add("enabled:btn-ghost", "enabled:btn-outline"); } else { // No member ID or editing - show save button and enable input memberIdInput.disabled = false; saveMemberId.textContent = "Save"; - saveMemberId.classList.remove("btn-ghost"); - saveMemberId.classList.add("btn-primary"); + saveMemberId.classList.remove("enabled:btn-ghost", "enabled:btn-outline"); + saveMemberId.classList.add("enabled:btn-primary"); } } @@ -275,6 +283,7 @@ export class StoreAuth { loadingSkeleton, officerViewToggle, officerContent, + sponsorViewToggle, } = this.elements; // Hide buttons initially @@ -284,16 +293,34 @@ export class StoreAuth { if (this.pb.authStore.isValid && this.pb.authStore.model) { // Update all the user information first const user = this.pb.authStore.model; + const isSponsor = user.member_type === "IEEE Sponsor"; userName.textContent = user.name || "Name not provided"; userEmail.textContent = user.email || "Email not available"; - // Enable member ID input and save button - memberIdInput.disabled = false; - saveMemberId.disabled = false; + // Hide member ID and resume sections for sponsors + const memberIdSection = memberIdInput.closest('.space-y-1') as HTMLElement; + const resumeSection = resumeUpload.closest('.space-y-2')?.parentElement as HTMLElement; + const memberIdDivider = memberIdSection?.nextElementSibling as HTMLElement; + const resumeDivider = resumeSection?.nextElementSibling as HTMLElement; - // Enable resume upload - resumeUpload.disabled = false; + if (isSponsor) { + // Hide member ID and resume sections for sponsors + if (memberIdSection) memberIdSection.style.display = 'none'; + if (memberIdDivider) memberIdDivider.style.display = 'none'; + if (resumeSection) resumeSection.style.display = 'none'; + if (resumeDivider) resumeDivider.style.display = 'none'; + } else { + // Show and enable member ID input and save button for non-sponsors + if (memberIdSection) memberIdSection.style.display = ''; + if (memberIdDivider) memberIdDivider.style.display = ''; + if (resumeSection) resumeSection.style.display = ''; + if (resumeDivider) resumeDivider.style.display = ''; + + memberIdInput.disabled = false; + saveMemberId.disabled = false; + resumeUpload.disabled = false; + } // Update member status if (user.verified) { @@ -330,8 +357,22 @@ export class StoreAuth { memberStatus.classList.add("badge-warning"); // Red for administrators } else if (user.member_type === "IEEE Officer") { memberStatus.classList.add("badge-info"); // Blue for officers + } else if (user.member_type === "IEEE Sponsor") { + memberStatus.classList.add("badge-warning"); // Yellow for sponsors } else { - memberStatus.classList.add("badge-neutral"); // Yellow for regular members + memberStatus.classList.add("badge-neutral"); // Neutral for regular members + } + + // Handle view toggles visibility + const isOfficer = ["IEEE Officer", "IEEE Administrator"].includes(user.member_type || ""); + const isSponsor = user.member_type === "IEEE Sponsor"; + + officerViewToggle.style.display = isOfficer ? "block" : "none"; + sponsorViewToggle.style.display = isSponsor ? "block" : "none"; + + // If user is an officer or sponsor, preload the table data + if (isOfficer || isSponsor) { + await this.fetchUserResumes(); } } else { memberStatus.textContent = "Not Verified"; @@ -369,20 +410,6 @@ export class StoreAuth { 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"); @@ -433,6 +460,7 @@ export class StoreAuth { loginButton.style.display = "block"; officerViewToggle.style.display = "none"; + sponsorViewToggle.style.display = "none"; } } @@ -586,43 +614,63 @@ export class StoreAuth { } private handleLogout() { + // Clear auth store this.pb.authStore.clear(); + + // Clear cached users + this.cachedUsers = []; + + // Reset member ID editing state + this.isEditingMemberId = false; + + // Show all sections that might have been hidden + const memberIdSection = this.elements.memberIdInput.closest('.space-y-1') as HTMLElement; + const resumeSection = this.elements.resumeUpload.closest('.space-y-2')?.parentElement as HTMLElement; + const memberIdDivider = memberIdSection?.nextElementSibling as HTMLElement; + const resumeDivider = resumeSection?.nextElementSibling as HTMLElement; + + // Show all sections + if (memberIdSection) memberIdSection.style.display = ''; + if (memberIdDivider) memberIdDivider.style.display = ''; + if (resumeSection) resumeSection.style.display = ''; + if (resumeDivider) resumeDivider.style.display = ''; + + // Update UI this.updateUI(); } private async fetchUserResumes(searchQuery: string = "") { try { - let filter = ""; // Remove the resume filter to show all users + // Only fetch from API if we don't have cached data + if (this.cachedUsers.length === 0) { + const records = await this.pb.collection("users").getList(1, 50, { + sort: "-updated", + fields: "id,name,email,member_id,resume,points,collectionId,collectionName", + expand: "resume", + }); + this.cachedUsers = records.items; + } + + // Filter cached data based on search query + let filteredUsers = this.cachedUsers; if (searchQuery) { - const terms = searchQuery - .toLowerCase() - .split(" ") - .filter((term) => term.length > 0); + 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; // Only apply search conditions + filteredUsers = this.cachedUsers.filter(user => { + return terms.every(term => + (user.name?.toLowerCase().includes(term) || + user.email?.toLowerCase().includes(term) || + user.member_id?.toLowerCase().includes(term)) + ); + }); } } - const records = await this.pb.collection("users").getList(1, 50, { - filter, - sort: "-updated", - fields: - "id,name,email,member_id,resume,points,collectionId,collectionName", - expand: "resume", - }); - - console.log("Fetched records:", records.items); // Debug log - const { resumeList } = this.elements; const fragment = document.createDocumentFragment(); + const isSponsor = this.pb.authStore.model?.member_type === "IEEE Sponsor"; - if (records.items.length === 0) { + if (filteredUsers.length === 0) { const row = document.createElement("tr"); row.innerHTML = ` @@ -631,14 +679,21 @@ export class StoreAuth { `; fragment.appendChild(row); } else { - records.items.forEach((user) => { + filteredUsers.forEach((user) => { const row = document.createElement("tr"); - const resumeUrl = - user.resume && user.resume !== "" - ? this.pb.files.getURL(user, user.resume.toString()) - : null; + const resumeUrl = user.resume && user.resume !== "" + ? this.pb.files.getURL(user, user.resume.toString()) + : null; - console.log("User resume:", user.resume, "Resume URL:", resumeUrl); // Debug log + // Create edit button only if not a sponsor + const editButton = !isSponsor ? ` + + ` : ''; row.innerHTML = ` @@ -649,21 +704,11 @@ export class StoreAuth {
ID: ${user.member_id || "N/A"}
Points: ${user.points || 0}
- ${ - resumeUrl - ? ` - - View Resume - - ` - : 'No resume' + ${resumeUrl + ? `View Resume` + : 'No resume' } - + ${editButton}
@@ -674,23 +719,13 @@ export class StoreAuth { ${user.member_id || "N/A"} ${user.points || 0} - ${ - resumeUrl - ? ` - - View Resume - - ` - : 'No resume' + ${resumeUrl + ? `View Resume` + : 'No resume' } - + ${editButton} `; @@ -701,16 +736,18 @@ export class StoreAuth { resumeList.innerHTML = ""; resumeList.appendChild(fragment); - // Setup edit profile event listeners - const editButtons = resumeList.querySelectorAll(".edit-profile"); - editButtons.forEach((button) => { - button.addEventListener("click", () => { - const userId = (button as HTMLButtonElement).dataset.userId; - if (userId) { - this.handleProfileEdit(userId); - } + // Setup edit profile event listeners only if not a sponsor + if (!isSponsor) { + const editButtons = resumeList.querySelectorAll(".edit-profile"); + editButtons.forEach((button) => { + button.addEventListener("click", () => { + const userId = (button as HTMLButtonElement).dataset.userId; + if (userId) { + this.handleProfileEdit(userId); + } + }); }); - }); + } } catch (err) { console.error("Failed to fetch user resumes:", err); const { resumeList } = this.elements; @@ -852,23 +889,19 @@ export class StoreAuth { this.handleMemberIdButton(), ); - // Search functionality with minimal debounce - let searchTimeout: NodeJS.Timeout; + // Search functionality 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 - }); + // Real-time search + this.elements.resumeSearch.addEventListener("input", handleSearch); - // Keep the click handler for the search button + // Search button click handler this.elements.searchResumes.addEventListener("click", handleSearch); - // Officer view toggle event listener - now just toggles visibility + // Officer view toggle event listener this.elements.officerViewCheckbox.addEventListener("change", (e) => { const isChecked = (e.target as HTMLInputElement).checked; const storeItemsContainer = document.getElementById("storeItemsGrid"); @@ -878,11 +911,40 @@ export class StoreAuth { storeItemsContainer.style.display = isChecked ? "none" : "grid"; } officerContent.style.display = isChecked ? "block" : "none"; + + // Uncheck sponsor view if officer view is checked + if (isChecked) { + const sponsorCheckbox = this.elements.sponsorViewToggle.querySelector('input[type="checkbox"]') as HTMLInputElement; + if (sponsorCheckbox) { + sponsorCheckbox.checked = false; + } + } }); + // Sponsor view toggle event listener + const sponsorCheckbox = this.elements.sponsorViewToggle.querySelector('input[type="checkbox"]') as HTMLInputElement; + if (sponsorCheckbox) { + sponsorCheckbox.addEventListener("change", (e) => { + const isChecked = (e.target as HTMLInputElement).checked; + const storeItemsContainer = document.getElementById("storeItemsGrid"); + const { officerContent } = this.elements; + + if (storeItemsContainer) { + storeItemsContainer.style.display = isChecked ? "none" : "grid"; + } + officerContent.style.display = isChecked ? "block" : "none"; + + // Uncheck officer view if sponsor view is checked + if (isChecked) { + this.elements.officerViewCheckbox.checked = false; + } + }); + } + // Refresh resumes button event listener this.elements.refreshResumes.addEventListener("click", () => { this.elements.resumeSearch.value = ""; // Clear search when refreshing + this.cachedUsers = []; // Clear the cache to force a new fetch this.fetchUserResumes(); }); diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro index c1e350d..3505a16 100644 --- a/src/components/auth/UserProfile.astro +++ b/src/components/auth/UserProfile.astro @@ -110,6 +110,17 @@ />
+
@@ -123,7 +134,7 @@ />
From 06c0155c82e24bd029fdeea76d1c510bb056b37d Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 28 Jan 2025 17:25:10 -0800 Subject: [PATCH 012/224] add yaml support --- bun.lock | 4 ++++ package.json | 2 ++ 2 files changed, 6 insertions(+) diff --git a/bun.lock b/bun.lock index 282fbe1..4ee959a 100644 --- a/bun.lock +++ b/bun.lock @@ -7,11 +7,13 @@ "@astrojs/node": "^9.0.0", "@astrojs/react": "4.1.2", "@astrojs/tailwind": "5.1.4", + "@types/js-yaml": "^4.0.9", "@types/react": "^18.3.14", "@types/react-dom": "^18.3.2", "astro": "5.1.1", "astro-expressive-code": "^0.38.3", "astro-icon": "^1.1.4", + "js-yaml": "^4.1.0", "motion": "^11.15.0", "next": "^15.1.2", "pocketbase": "^0.25.1", @@ -342,6 +344,8 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], diff --git a/package.json b/package.json index 45d74f2..56fcc98 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "@astrojs/node": "^9.0.0", "@astrojs/react": "4.1.2", "@astrojs/tailwind": "5.1.4", + "@types/js-yaml": "^4.0.9", "@types/react": "^18.3.14", "@types/react-dom": "^18.3.2", "astro": "5.1.1", "astro-expressive-code": "^0.38.3", "astro-icon": "^1.1.4", + "js-yaml": "^4.1.0", "motion": "^11.15.0", "next": "^15.1.2", "pocketbase": "^0.25.1", From 08a6a8a15cb52d704fed520c47303b26e4b16c39 Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 28 Jan 2025 17:25:21 -0800 Subject: [PATCH 013/224] fix width for online-store --- src/pages/online-store.astro | 221 ++++++++++++++++++----------------- 1 file changed, 113 insertions(+), 108 deletions(-) diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro index 42dc43c..626cf5e 100644 --- a/src/pages/online-store.astro +++ b/src/pages/online-store.astro @@ -6,119 +6,124 @@ const title = "IEEE Store"; --- -
-

IEEE UCSD Store

-
- -
- -
+
+

IEEE UCSD Store

+
+ +
+ +
- -
-
-
-
-
-

- Store Coming Soon! -

-
-

- Our store is currently under development. Check back later - for IEEE UCSD merchandise! In the meantime, please make sure - your profile is up to date with your IEEE Member ID and - resume. -

+ +
+
+
+
+
+

+ Store Coming Soon! +

+
+

+ Our store is currently under + development. Check back later for IEEE + UCSD merchandise! In the meantime, + please make sure your profile is up to + date with your IEEE Member ID and + resume. +

+
+
+
+
+
+ + -
-
- - -
-
-
+
From 4f31592aa1b398d1afcfa1df0a43f753117955ef Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 28 Jan 2025 17:25:52 -0800 Subject: [PATCH 014/224] add config, pdf, and partial documentation support --- src/components/auth/StoreAuth.ts | 232 ++++++++++++++++++++------ src/components/auth/UserProfile.astro | 44 +++++ src/data/storeConfig.yaml | 70 ++++++++ 3 files changed, 292 insertions(+), 54 deletions(-) create mode 100644 src/data/storeConfig.yaml diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts index 8511a8d..6a2cf78 100644 --- a/src/components/auth/StoreAuth.ts +++ b/src/components/auth/StoreAuth.ts @@ -1,4 +1,80 @@ 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 { loginButton: HTMLButtonElement; @@ -34,6 +110,10 @@ interface AuthElements { editorCurrentResume: HTMLParagraphElement; saveProfileButton: HTMLButtonElement; sponsorViewToggle: HTMLDivElement; + pdfViewer: HTMLDialogElement; + pdfFrame: HTMLIFrameElement; + pdfTitle: HTMLHeadingElement; + pdfExternalLink: HTMLAnchorElement; } export class StoreAuth { @@ -41,9 +121,10 @@ export class StoreAuth { private elements: AuthElements & { loadingSkeleton: HTMLDivElement }; private isEditingMemberId: boolean = false; private cachedUsers: any[] = []; // Store users data + private config = config; constructor() { - this.pb = new PocketBase("https://pocketbase.ieeeucsd.org"); + this.pb = new PocketBase(this.config.api.baseUrl); this.elements = this.getElements(); this.init(); } @@ -167,6 +248,11 @@ export class StoreAuth { "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; + if ( !loginButton || !logoutButton || @@ -201,7 +287,11 @@ export class StoreAuth { !editorResume || !editorCurrentResume || !saveProfileButton || - !sponsorViewToggle + !sponsorViewToggle || + !pdfViewer || + !pdfFrame || + !pdfTitle || + !pdfExternalLink ) { throw new Error("Required DOM elements not found"); } @@ -241,6 +331,10 @@ export class StoreAuth { editorCurrentResume, saveProfileButton, sponsorViewToggle, + pdfViewer, + pdfFrame, + pdfTitle, + pdfExternalLink, }; } @@ -293,10 +387,10 @@ export class StoreAuth { if (this.pb.authStore.isValid && this.pb.authStore.model) { // Update all the user information first 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"; - userEmail.textContent = user.email || "Email not available"; + userName.textContent = user.name || this.config.ui.messages.auth.notProvided; + userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable; // Hide member ID and resume sections for sponsors const memberIdSection = memberIdInput.closest('.space-y-1') as HTMLElement; @@ -327,11 +421,10 @@ 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 isIeeeOfficer = user.email?.toLowerCase().endsWith(this.config.autoDetection.officer.emailDomain) || false; const newMemberType = isIeeeOfficer - ? "IEEE Officer" - : "Regular Member"; + ? this.config.roles.officer.name + : this.config.roles.member.name; await this.pb.collection("users").update(user.id, { 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( "badge-neutral", "badge-success", @@ -353,19 +446,19 @@ export class StoreAuth { ); // Set color based on member type - if (user.member_type === "IEEE Administrator") { - memberStatus.classList.add("badge-warning"); // Red for administrators - } else if (user.member_type === "IEEE Officer") { - memberStatus.classList.add("badge-info"); // Blue for officers - } else if (user.member_type === "IEEE Sponsor") { - memberStatus.classList.add("badge-warning"); // Yellow for sponsors + const role = Object.values(this.config.roles).find(r => r.name === user.member_type); + if (role) { + memberStatus.classList.add(role.badge); } else { - memberStatus.classList.add("badge-neutral"); // Neutral for regular members + memberStatus.classList.add(this.config.roles.member.badge); } // Handle view toggles visibility - const isOfficer = ["IEEE Officer", "IEEE Administrator"].includes(user.member_type || ""); - const isSponsor = user.member_type === "IEEE Sponsor"; + const isOfficer = [ + 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"; sponsorViewToggle.style.display = isSponsor ? "block" : "none"; @@ -375,7 +468,7 @@ export class StoreAuth { await this.fetchUserResumes(); } } else { - memberStatus.textContent = "Not Verified"; + memberStatus.textContent = this.config.ui.messages.auth.notVerified; memberStatus.classList.remove( "badge-info", "badge-warning", @@ -392,7 +485,7 @@ export class StoreAuth { // Update last login const lastLoginDate = user.last_login ? new Date(user.last_login).toLocaleString() - : "Never"; + : this.config.ui.messages.auth.never; lastLogin.textContent = lastLoginDate; // Update resume section @@ -401,12 +494,19 @@ export class StoreAuth { (!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); + 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"; } @@ -421,9 +521,9 @@ export class StoreAuth { logoutButton.style.display = "block"; } else { // Update for logged out state - userName.textContent = "Not signed in"; - userEmail.textContent = "Not signed in"; - memberStatus.textContent = "Not verified"; + userName.textContent = this.config.ui.messages.auth.notSignedIn; + userEmail.textContent = this.config.ui.messages.auth.notSignedIn; + memberStatus.textContent = this.config.ui.messages.auth.notVerified; memberStatus.classList.remove( "badge-info", "badge-warning", @@ -431,7 +531,7 @@ export class StoreAuth { "badge-error", ); memberStatus.classList.add("badge-neutral"); - lastLogin.textContent = "Never"; + lastLogin.textContent = this.config.ui.messages.auth.never; // Disable member ID input and save button memberIdInput.disabled = true; @@ -448,6 +548,7 @@ export class StoreAuth { // Reset resume section resumeName.textContent = "No resume uploaded"; resumeDownload.href = "#"; + resumeDownload.onclick = null; resumeActions.style.display = "none"; // After everything is updated, show the content @@ -492,7 +593,7 @@ export class StoreAuth { const memberId = memberIdInput.value.trim(); try { - memberIdStatus.textContent = "Saving member ID..."; + memberIdStatus.textContent = this.config.ui.messages.memberId.saving; const user = this.pb.authStore.model; if (!user?.id) { @@ -503,26 +604,36 @@ export class StoreAuth { member_id: memberId, }); - memberIdStatus.textContent = "IEEE Member ID saved successfully!"; + memberIdStatus.textContent = this.config.ui.messages.memberId.success; this.isEditingMemberId = false; this.updateUI(); // Clear the status message after a delay setTimeout(() => { memberIdStatus.textContent = ""; - }, 3000); + }, this.config.ui.messages.memberId.messageTimeout); } catch (err: any) { console.error("IEEE Member ID save error:", err); - memberIdStatus.textContent = - "Failed to save IEEE Member ID. Please try again."; + memberIdStatus.textContent = this.config.ui.messages.memberId.error; } } private async handleResumeUpload(file: File) { 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 { - uploadStatus.textContent = "Uploading resume..."; + uploadStatus.textContent = this.config.ui.messages.resume.uploading; const formData = new FormData(); formData.append("resume", file); @@ -543,7 +654,7 @@ export class StoreAuth { await this.pb.collection("users").update(user.id, formData); - uploadStatus.textContent = "Resume uploaded successfully!"; + uploadStatus.textContent = this.config.ui.messages.resume.success; this.updateUI(); // Clear the file input @@ -552,10 +663,10 @@ export class StoreAuth { // Clear the status message after a delay setTimeout(() => { uploadStatus.textContent = ""; - }, 3000); + }, this.config.ui.messages.resume.messageTimeout); } catch (err: any) { 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; try { - uploadStatus.textContent = "Deleting resume..."; + uploadStatus.textContent = this.config.ui.messages.resume.deleting; const user = this.pb.authStore.model; if (!user?.id) { @@ -574,16 +685,16 @@ export class StoreAuth { resume: null, }); - uploadStatus.textContent = "Resume deleted successfully!"; + uploadStatus.textContent = this.config.ui.messages.resume.deleteSuccess; this.updateUI(); // Clear the status message after a delay setTimeout(() => { uploadStatus.textContent = ""; - }, 3000); + }, this.config.ui.messages.resume.messageTimeout); } catch (err: any) { 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 { 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 === this.config.api.oauth2.providerName, ); if (!oidcProvider) { @@ -603,12 +714,12 @@ export class StoreAuth { localStorage.setItem("provider", JSON.stringify(oidcProvider)); // 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); window.location.href = authUrl; } catch (err: any) { 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"; } } @@ -643,8 +754,8 @@ export class StoreAuth { try { // Only fetch from API if we don't have cached data if (this.cachedUsers.length === 0) { - const records = await this.pb.collection("users").getList(1, 50, { - sort: "-updated", + const records = await this.pb.collection("users").getList(1, this.config.ui.defaults.pageSize, { + sort: this.config.ui.defaults.sortField, fields: "id,name,email,member_id,resume,points,collectionId,collectionName", expand: "resume", }); @@ -668,7 +779,7 @@ export class StoreAuth { const { resumeList } = this.elements; 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) { const row = document.createElement("tr"); @@ -684,6 +795,7 @@ export class StoreAuth { const resumeUrl = user.resume && user.resume !== "" ? this.pb.files.getURL(user, user.resume.toString()) : null; + const fileName = resumeUrl ? this.getFileNameFromUrl(user.resume.toString()) : null; // Create edit button only if not a sponsor const editButton = !isSponsor ? ` @@ -695,6 +807,11 @@ export class StoreAuth { ` : ''; + // Create view resume link + const viewResumeLink = resumeUrl + ? `View Resume` + : 'No resume'; + row.innerHTML = ` @@ -704,10 +821,7 @@ export class StoreAuth {
ID: ${user.member_id || "N/A"}
Points: ${user.points || 0}
- ${resumeUrl - ? `View Resume` - : 'No resume' - } + ${viewResumeLink} ${editButton}
@@ -719,10 +833,7 @@ export class StoreAuth { ${user.member_id || "N/A"} ${user.points || 0} - ${resumeUrl - ? `View Resume` - : 'No resume' - } + ${viewResumeLink} ${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() { // Initial UI update with loading state this.updateUI().catch(console.error); @@ -969,5 +1088,10 @@ 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); } } diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro index 3505a16..6082a52 100644 --- a/src/components/auth/UserProfile.astro +++ b/src/components/auth/UserProfile.astro @@ -289,6 +289,50 @@ + + + + + + + + diff --git a/src/components/profile/DefaultProfileView.astro b/src/components/profile/DefaultProfileView.astro new file mode 100644 index 0000000..91cf831 --- /dev/null +++ b/src/components/profile/DefaultProfileView.astro @@ -0,0 +1,439 @@ +
+ +
+
+ +
+
+

Events

+
+ + +
+
+
+
+
+
+
+
+
+ + + +
+
+ + +
+
+ + { + Array(3) + .fill(0) + .map(() => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + } +
+
+
+
+ + +
+
+

Something here

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + diff --git a/src/components/profile/EventEditor.astro b/src/components/profile/EventEditor.astro new file mode 100644 index 0000000..a37159f --- /dev/null +++ b/src/components/profile/EventEditor.astro @@ -0,0 +1,270 @@ +--- +import yaml from "js-yaml"; +import configYaml from "../../data/storeConfig.yaml?raw"; + +const config = yaml.load(configYaml) as any; +const { editor_title, form } = config.ui.tables.events; +--- + + + + + + diff --git a/src/components/profile/EventManagement.astro b/src/components/profile/EventManagement.astro new file mode 100644 index 0000000..cad74f2 --- /dev/null +++ b/src/components/profile/EventManagement.astro @@ -0,0 +1,260 @@ +--- +import EventEditor from "./EventEditor.astro"; +import yaml from "js-yaml"; +import configYaml from "../../data/storeConfig.yaml?raw"; + +const config = yaml.load(configYaml) as any; +const { title, columns } = config.ui.tables.events; +--- + +
+

{title}

+
+
+ +
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + diff --git a/src/components/store/MemberManagement.astro b/src/components/profile/MemberManagement.astro similarity index 93% rename from src/components/store/MemberManagement.astro rename to src/components/profile/MemberManagement.astro index ca2a9da..1e722cc 100644 --- a/src/components/store/MemberManagement.astro +++ b/src/components/profile/MemberManagement.astro @@ -49,7 +49,9 @@
- +
@@ -60,7 +62,7 @@ - +
diff --git a/src/components/store/OfficerStoreView.astro b/src/components/profile/OfficerView.astro similarity index 100% rename from src/components/store/OfficerStoreView.astro rename to src/components/profile/OfficerView.astro diff --git a/src/components/store/EventEditor.astro b/src/components/store/EventEditor.astro deleted file mode 100644 index 062574c..0000000 --- a/src/components/store/EventEditor.astro +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/src/components/store/EventManagement.astro b/src/components/store/EventManagement.astro deleted file mode 100644 index 11428b6..0000000 --- a/src/components/store/EventManagement.astro +++ /dev/null @@ -1,82 +0,0 @@ ---- -import EventEditor from "./EventEditor.astro"; ---- - -
-

Event Management

-
-
- -
-
- - -
-
-
-
- - - - - - - - - - - - - - - - -
-
- - - - diff --git a/src/data/storeConfig.yaml b/src/data/storeConfig.yaml index 3cf86d0..a431843 100644 --- a/src/data/storeConfig.yaml +++ b/src/data/storeConfig.yaml @@ -61,6 +61,14 @@ ui: deleteSuccess: Event deleted successfully! deleteError: Failed to delete event. Please try again. messageTimeout: 3000 + checkIn: + checking: Checking event code... + success: Successfully checked in to event! + error: Failed to check in. Please try again. + invalid: Invalid event code. Please try again. + expired: This event is not currently active. + alreadyCheckedIn: You have already checked in to this event. + messageTimeout: 3000 auth: loginError: Failed to start authentication @@ -70,6 +78,55 @@ ui: notAvailable: Not available never: Never + tables: + events: + title: Event Management + editor_title: Event Details + columns: + event_name: Event Name + event_id: Event ID + event_code: Event Code + start_date: Start Date + end_date: End Date + points_to_reward: Points to Reward + location: Location + registered_users: Registered + actions: Actions + form: + event_id: + label: Event ID + placeholder: Enter unique event ID + event_name: + label: Event Name + placeholder: Enter event name + event_code: + label: Event Code + placeholder: Enter check-in code + start_date: + date_label: Start Date for check-in + time_label: Start Time for check-in + date_placeholder: Select start date for check-in + time_placeholder: Select start time for check-in + end_date: + date_label: End Date for check-in + time_label: End Time for check-in + date_placeholder: Select end date for check-in + time_placeholder: Select end time for check-in + points_to_reward: + label: Points to Reward + placeholder: Enter points value + location: + label: Location + placeholder: Enter event location + files: + label: Event Files + help_text: Upload event-related files (PDF, DOC, DOCX, TXT, JPG, JPEG, PNG) + buttons: + save: Save + cancel: Cancel + edit: Edit + delete: Delete + defaults: pageSize: 50 sortField: -updated diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro index 4d8f42f..de3db0e 100644 --- a/src/pages/online-store.astro +++ b/src/pages/online-store.astro @@ -2,8 +2,8 @@ import Layout from "../layouts/Layout.astro"; import UserProfile from "../components/auth/UserProfile.astro"; import DefaultStoreView from "../components/store/DefaultStoreView.astro"; -import OfficerStoreView from "../components/store/OfficerStoreView.astro"; -const title = "IEEE Store"; +import OfficerStoreView from "../components/profile/OfficerView.astro"; +const title = "IEEE Online Store"; --- diff --git a/src/pages/profile.astro b/src/pages/profile.astro new file mode 100644 index 0000000..78b2745 --- /dev/null +++ b/src/pages/profile.astro @@ -0,0 +1,50 @@ +--- +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"; +const title = "User Profile"; +--- + + +
+

Profile Management

+
+ +
+ +
+ + +
+
+ +
+ +
+
+
+
+ + From 5f00e88c687c05562954028bcacfc66874990f97 Mon Sep 17 00:00:00 2001 From: chark1es Date: Fri, 31 Jan 2025 23:28:55 -0800 Subject: [PATCH 018/224] fix file deletion --- src/components/auth/EventAuth.ts | 9 +++++---- src/components/profile/EventEditor.astro | 7 ++++++- src/components/reimbursement/Reimbursement.astro | 0 src/pages/reimbursement.astro | 7 +++++++ 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 src/components/reimbursement/Reimbursement.astro create mode 100644 src/pages/reimbursement.astro diff --git a/src/components/auth/EventAuth.ts b/src/components/auth/EventAuth.ts index 6fa72cc..4b244d0 100644 --- a/src/components/auth/EventAuth.ts +++ b/src/components/auth/EventAuth.ts @@ -1183,13 +1183,14 @@ export class EventAuth { // Add delete handler const deleteButton = fileItem.querySelector('.text-error'); if (deleteButton) { - deleteButton.addEventListener('click', async () => { + deleteButton.addEventListener('click', async (e) => { + e.preventDefault(); // Prevent any form submission if (confirm('Are you sure you want to remove this file?')) { try { const fileToRemove = deleteButton.getAttribute('data-file'); if (!fileToRemove) throw new Error('File not found'); - // Get the current event data to ensure we have the latest state + // Get the current event data const currentEvent = await this.pb.collection('events').getOne(event.id); // Filter out the file to be removed @@ -1200,14 +1201,14 @@ export class EventAuth { files: updatedFiles }); - // Update the event.files array in memory + // Update the local event object event.files = updatedFiles; // Remove the file item from the UI fileItem.remove(); // If no files left, show the "No files" message - if (updatedFiles.length === 0) { + if (!event.files || event.files.length === 0) { currentFiles.innerHTML = 'No files'; } } catch (err) { diff --git a/src/components/profile/EventEditor.astro b/src/components/profile/EventEditor.astro index a37159f..dc2f2be 100644 --- a/src/components/profile/EventEditor.astro +++ b/src/components/profile/EventEditor.astro @@ -10,7 +10,12 @@ const { editor_title, form } = config.ui.tables.events; + + + + + + diff --git a/src/components/profile/DefaultProfileView.astro b/src/components/profile/DefaultProfileView.astro index 22b0528..031f70d 100644 --- a/src/components/profile/DefaultProfileView.astro +++ b/src/components/profile/DefaultProfileView.astro @@ -8,48 +8,44 @@

Events

- -
-
-
-
-
-
-
-
-
+ +
+
+

Quick Check-in

- @@ -214,8 +217,14 @@ const title = "User Profile"; From 6b9d7935907a3fa1c205924e519655286d444f2f Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 03:07:46 -0800 Subject: [PATCH 040/224] fix default profile view --- src/components/pocketbase/FileManager.ts | 156 ++++++++ .../profile/DefaultProfileView.astro | 350 +++++++++++------- src/components/profile/EventEditor.astro | 159 ++++++++ src/components/profile/UserSettings.astro | 12 +- src/pages/profile.astro | 4 +- 5 files changed, 533 insertions(+), 148 deletions(-) create mode 100644 src/components/pocketbase/FileManager.ts diff --git a/src/components/pocketbase/FileManager.ts b/src/components/pocketbase/FileManager.ts new file mode 100644 index 0000000..4871bb3 --- /dev/null +++ b/src/components/pocketbase/FileManager.ts @@ -0,0 +1,156 @@ +import { Authentication } from "./Authentication"; + +export class FileManager { + private auth: Authentication; + private static instance: FileManager; + + private constructor() { + this.auth = Authentication.getInstance(); + } + + /** + * Get the singleton instance of FileManager + */ + public static getInstance(): FileManager { + if (!FileManager.instance) { + FileManager.instance = new FileManager(); + } + return FileManager.instance; + } + + /** + * Upload a single file to a record + * @param collectionName The name of the collection + * @param recordId The ID of the record to attach the file to + * @param field The field name for the file + * @param file The file to upload + * @returns The updated record + */ + public async uploadFile( + collectionName: string, + recordId: string, + field: string, + file: File + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to upload files"); + } + + try { + const pb = this.auth.getPocketBase(); + const formData = new FormData(); + formData.append(field, file); + + return await pb.collection(collectionName).update(recordId, formData); + } catch (err) { + console.error(`Failed to upload file to ${collectionName}:`, err); + throw err; + } + } + + /** + * Upload multiple files to a record + * @param collectionName The name of the collection + * @param recordId The ID of the record to attach the files to + * @param field The field name for the files + * @param files Array of files to upload + * @returns The updated record + */ + public async uploadFiles( + collectionName: string, + recordId: string, + field: string, + files: File[] + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to upload files"); + } + + try { + const pb = this.auth.getPocketBase(); + const formData = new FormData(); + + files.forEach(file => { + formData.append(field, file); + }); + + return await pb.collection(collectionName).update(recordId, formData); + } catch (err) { + console.error(`Failed to upload files to ${collectionName}:`, err); + throw err; + } + } + + /** + * Get the URL for a file + * @param collectionName The name of the collection + * @param recordId The ID of the record containing the file + * @param filename The name of the file + * @returns The URL to access the file + */ + public getFileUrl( + collectionName: string, + recordId: string, + filename: string + ): string { + const pb = this.auth.getPocketBase(); + return `${pb.baseUrl}/api/files/${collectionName}/${recordId}/${filename}`; + } + + /** + * Delete a file from a record + * @param collectionName The name of the collection + * @param recordId The ID of the record containing the file + * @param field The field name of the file to delete + * @returns The updated record + */ + public async deleteFile( + collectionName: string, + recordId: string, + field: string + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to delete files"); + } + + try { + const pb = this.auth.getPocketBase(); + const data = { [field]: null }; + return await pb.collection(collectionName).update(recordId, data); + } catch (err) { + console.error(`Failed to delete file from ${collectionName}:`, err); + throw err; + } + } + + /** + * Download a file + * @param collectionName The name of the collection + * @param recordId The ID of the record containing the file + * @param filename The name of the file + * @returns The file blob + */ + public async downloadFile( + collectionName: string, + recordId: string, + filename: string + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to download files"); + } + + try { + const url = this.getFileUrl(collectionName, recordId, filename); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.blob(); + } catch (err) { + console.error(`Failed to download file from ${collectionName}:`, err); + throw err; + } + } +} \ No newline at end of file diff --git a/src/components/profile/DefaultProfileView.astro b/src/components/profile/DefaultProfileView.astro index 64efa83..58ae144 100644 --- a/src/components/profile/DefaultProfileView.astro +++ b/src/components/profile/DefaultProfileView.astro @@ -184,64 +184,68 @@ import { Authentication } from "../pocketbase/Authentication"; import { Get } from "../pocketbase/Get"; import { SendLog } from "../pocketbase/SendLog"; + import { FileManager } from "../pocketbase/FileManager"; const auth = Authentication.getInstance(); const get = Get.getInstance(); const logger = SendLog.getInstance(); + const fileManager = FileManager.getInstance(); - // Initialize event check-in - document.addEventListener("DOMContentLoaded", async () => { + // Track if we're currently fetching events + let isFetchingEvents = false; + let lastFetchPromise: Promise | null = null; + + // Define interfaces + interface BaseRecord { + id: string; + [key: string]: any; + } + + interface Event extends BaseRecord { + id: string; + event_id: string; + event_name: string; + start_date: string; + end_date: string; + location: string; + files?: string[]; + } + + // Add debounce function + function debounce any>( + func: T, + wait: number + ): (...args: Parameters) => void { + let timeout: ReturnType | null = null; + return (...args: Parameters) => { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; + } + + // Create debounced version of renderEvents + const debouncedRenderEvents = debounce(async () => { try { - // Get current user's events - if (auth.isAuthenticated()) { - const user = auth.getCurrentUser(); - if (user) { - // Get user's events - const events = await get.getMany( - "events", - user.events_attended || [] - ); - - // Update UI with events data - // ... rest of the code ... - } - } + await renderEvents(); } catch (error) { - console.error("Failed to initialize profile:", error); - await logger.send( - "error", - "profile view", - `Failed to initialize profile: ${error instanceof Error ? error.message : "Unknown error"}` - ); + console.error("Failed to render events:", error); } - }); - - // Handle loading states - const eventCheckInSkeleton = document.getElementById( - "eventCheckInSkeleton" - ); - const eventCheckInContent = document.getElementById("eventCheckInContent"); - const pastEventsCount = document.getElementById("pastEventsCount"); + }, 300); // Function to show content and hide skeleton function showEventCheckIn() { + const eventCheckInSkeleton = document.getElementById( + "eventCheckInSkeleton" + ); + const eventCheckInContent = document.getElementById( + "eventCheckInContent" + ); if (eventCheckInSkeleton && eventCheckInContent) { eventCheckInSkeleton.classList.add("hidden"); eventCheckInContent.classList.remove("hidden"); } } - // Show content when auth state changes - auth.onAuthStateChange(() => { - showEventCheckIn(); - renderEvents(); - }); - - // Show content on initial load if already authenticated - if (auth.isAuthenticated()) { - showEventCheckIn(); - } - // Function to format date function formatDate(dateStr: string): string { const date = new Date(dateStr); @@ -321,14 +325,6 @@ } } - interface Event extends RecordModel { - event_id: string; - event_name: string; - start_date: string; - end_date: string; - location: string; - } - // Function to render event card function renderEventCard(event: Event, attendedEvents: string[]): string { const isAttended = @@ -350,7 +346,7 @@ - ${event.files.length} File${event.files.length > 1 ? "s" : ""} + ${event.files?.length || 0} File${(event.files?.length || 0) > 1 ? "s" : ""} ` : ""; @@ -389,115 +385,181 @@ `; } - // Function to render events + // Function to render events with request cancellation handling async function renderEvents() { const eventsList = document.getElementById("eventsList"); const pastEventsList = document.getElementById("pastEventsList"); if (!eventsList || !pastEventsList) return; - try { - // Get current user's attended events with safe parsing - const user = auth.getCurrentUser(); - let attendedEvents: string[] = []; - - if (user?.events_attended) { - try { - attendedEvents = - typeof user.events_attended === "string" - ? JSON.parse(user.events_attended) - : Array.isArray(user.events_attended) - ? user.events_attended - : []; - } catch (e) { - console.warn("Failed to parse events_attended:", e); - attendedEvents = []; - } + // If we're already fetching, wait for the current fetch to complete + if (isFetchingEvents && lastFetchPromise) { + try { + await lastFetchPromise; + return; + } catch (err) { + console.warn("Previous fetch failed:", err); } + } - // Fetch all events - const events = await get.getMany( - "events", - user.events_attended || [] - ); + // Set up new fetch + isFetchingEvents = true; + const fetchPromise = (async () => { + try { + // Get current user's attended events with safe parsing + const user = auth.getCurrentUser(); + let attendedEvents: string[] = []; - // Clear loading skeletons - eventsList.innerHTML = ""; - pastEventsList.innerHTML = ""; - - // Categorize events - const now = new Date(); - const currentEvents: Event[] = []; - const upcomingEvents: Event[] = []; - const pastEvents: Event[] = []; - - events.forEach((event) => { - const typedEvent = event as Event; - const startDate = new Date(typedEvent.start_date); - const endDate = new Date(typedEvent.end_date); - - if (startDate > now) { - upcomingEvents.push(typedEvent); - } else if (endDate >= now && startDate <= now) { - currentEvents.push(typedEvent); - } else { - pastEvents.push(typedEvent); + if (user?.events_attended) { + try { + attendedEvents = + typeof user.events_attended === "string" + ? JSON.parse(user.events_attended) + : Array.isArray(user.events_attended) + ? user.events_attended + : []; + console.log("Attended events:", attendedEvents); + } catch (e) { + console.warn("Failed to parse events_attended:", e); + attendedEvents = []; + } } - }); - // Sort upcoming events by start date - const sortedUpcomingEvents = upcomingEvents.sort( - (a, b) => - new Date(a.start_date).getTime() - - new Date(b.start_date).getTime() - ); + // Fetch all events + console.log("Fetching all events"); + const events = await get.getAll( + "events", + undefined, + "-start_date" + ); + console.log("Fetched events:", events); - // Sort past events by date descending (most recent first) - const sortedPastEvents = pastEvents.sort( - (a, b) => - new Date(b.end_date).getTime() - - new Date(a.end_date).getTime() - ); + if (!Array.isArray(events)) { + throw new Error( + "Failed to fetch events: Invalid response format" + ); + } - // Update past events count - if (pastEventsCount) { - pastEventsCount.textContent = - sortedPastEvents.length.toString(); - } + // Clear loading skeletons + eventsList.innerHTML = ""; + pastEventsList.innerHTML = ""; - // Function to render section - function renderSection(events: Event[]): string { - if (events.length === 0) { - return ` -
-

No events found

+ // Categorize events + const now = new Date(); + const currentEvents: Event[] = []; + const upcomingEvents: Event[] = []; + const pastEvents: Event[] = []; + + events.forEach((event) => { + if (!event.start_date || !event.end_date) { + console.warn("Event missing dates:", event); + return; + } + + const startDate = new Date(event.start_date); + const endDate = new Date(event.end_date); + + if (startDate > now) { + upcomingEvents.push(event); + } else if (endDate >= now && startDate <= now) { + currentEvents.push(event); + } else { + pastEvents.push(event); + } + }); + + // Sort upcoming events by start date + const sortedUpcomingEvents = upcomingEvents.sort( + (a, b) => + new Date(a.start_date).getTime() - + new Date(b.start_date).getTime() + ); + + // Sort past events by date descending (most recent first) + const sortedPastEvents = pastEvents.sort( + (a, b) => + new Date(b.end_date).getTime() - + new Date(a.end_date).getTime() + ); + + // Update past events count + const pastEventsCountElement = + document.getElementById("pastEventsCount"); + if (pastEventsCountElement) { + pastEventsCountElement.textContent = + sortedPastEvents.length.toString(); + } + + // Function to render section + function renderSection(events: Event[]): string { + if (events.length === 0) { + return ` +
+

No events found

+
+ `; + } + return events + .map((event) => renderEventCard(event, attendedEvents)) + .join(""); + } + + // Update main events list (current & upcoming) + eventsList.innerHTML = renderSection([ + ...currentEvents, + ...sortedUpcomingEvents, + ]); + + // Update past events list + pastEventsList.innerHTML = renderSection(sortedPastEvents); + } catch (err) { + console.error("Failed to render events:", err); + await logger.send( + "error", + "render events", + `Failed to render events: ${err instanceof Error ? err.message : "Unknown error"}` + ); + const errorMessage = ` +
+

Failed to load events. Please try again later.

+

${err instanceof Error ? err.message : "Unknown error"}

`; - } - return events - .map((event) => renderEventCard(event, attendedEvents)) - .join(""); + eventsList.innerHTML = errorMessage; + pastEventsList.innerHTML = errorMessage; + throw err; // Re-throw to handle in the outer catch + } finally { + isFetchingEvents = false; } + })(); - // Update main events list (current & upcoming) - eventsList.innerHTML = renderSection([ - ...currentEvents, - ...sortedUpcomingEvents, - ]); + lastFetchPromise = fetchPromise; - // Update past events list - pastEventsList.innerHTML = renderSection(sortedPastEvents); + try { + await fetchPromise; } catch (err) { - console.error("Failed to render events:", err); - const errorMessage = ` -
-

Failed to load events. Please try again later.

-
- `; - eventsList.innerHTML = errorMessage; - pastEventsList.innerHTML = errorMessage; + // Error already handled above + console.debug("Fetch completed with error"); } } + // Initialize event check-in + document.addEventListener("DOMContentLoaded", () => { + showEventCheckIn(); + // Only render events once at startup + renderEvents().catch(console.error); + }); + + // Show content when auth state changes + let lastAuthState = auth.isAuthenticated(); + auth.onAuthStateChange((isValid) => { + showEventCheckIn(); + // Only re-render if auth state actually changed + if (lastAuthState !== isValid) { + lastAuthState = isValid; + renderEvents().catch(console.error); + } + }); + // Add event listener for viewing files interface ViewEventFilesEvent extends CustomEvent { detail: { @@ -509,7 +571,10 @@ if (e instanceof CustomEvent && "eventId" in e.detail) { (async () => { try { - const event = await get.getOne("events", e.detail.eventId); + const event = await get.getOne( + "events", + e.detail.eventId + ); const fileViewerContent = document.getElementById("fileViewerContent"); const fileViewerTitle = @@ -533,7 +598,11 @@ fileViewerContent.innerHTML = event.files .map((file) => { - const fileUrl = get.getFileURL(event, file); + const fileUrl = fileManager.getFileUrl( + "events", + event.id, + file + ); const fileName = file.split("/").pop() || "File"; const fileExt = @@ -599,7 +668,4 @@ })(); } }) as unknown as EventListener); - - // Initial render - renderEvents(); diff --git a/src/components/profile/EventEditor.astro b/src/components/profile/EventEditor.astro index 7d16f0a..8d6471a 100644 --- a/src/components/profile/EventEditor.astro +++ b/src/components/profile/EventEditor.astro @@ -285,3 +285,162 @@ const { editor_title, form } = text.ui.tables.events; + + diff --git a/src/components/profile/UserSettings.astro b/src/components/profile/UserSettings.astro index 08c250f..7f0b904 100644 --- a/src/components/profile/UserSettings.astro +++ b/src/components/profile/UserSettings.astro @@ -274,10 +274,12 @@ const majorsList: string[] = allMajors import { Authentication } from "../pocketbase/Authentication"; import { Update } from "../pocketbase/Update"; import { SendLog } from "../pocketbase/SendLog"; + import { FileManager } from "../pocketbase/FileManager"; const auth = Authentication.getInstance(); const update = Update.getInstance(); const logger = SendLog.getInstance(); + const fileManager = FileManager.getInstance(); // Get form elements const memberIdInput = document.getElementById( @@ -355,10 +357,12 @@ const majorsList: string[] = allMajors const user = auth.getCurrentUser(); if (!user) throw new Error("User not authenticated"); - const formData = new FormData(); - formData.append("resume", file); - - await update.updateFields("users", user.id, formData); + await fileManager.uploadFile( + "users", + user.id, + "resume", + file + ); uploadStatus.textContent = "Resume uploaded successfully"; if (currentResume) currentResume.textContent = file.name; diff --git a/src/pages/profile.astro b/src/pages/profile.astro index 2d6672c..ed4827a 100644 --- a/src/pages/profile.astro +++ b/src/pages/profile.astro @@ -201,10 +201,10 @@ const text = yaml.load(textConfig) as any;
-
@@ -345,19 +345,20 @@ const text = yaml.load(textConfig) as any; // Update content visibility const tabId = (tab as HTMLElement).dataset.defaultTab; + const views = { + events: defaultView, + settings: settingsView, + }; - if (defaultView && settingsView) { - defaultView.classList.add("hidden"); - settingsView.classList.add("hidden"); + // Hide all views first + Object.values(views).forEach((view) => { + if (view) view.classList.add("hidden"); + }); - switch (tabId) { - case "events": - defaultView.classList.remove("hidden"); - break; - case "settings": - settingsView.classList.remove("hidden"); - break; - } + // Show the selected view + const selectedView = views[tabId as keyof typeof views]; + if (selectedView) { + selectedView.classList.remove("hidden"); } }); }); From 00edb569c3defb39a140552ee2e2ba0db01a796b Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 03:25:36 -0800 Subject: [PATCH 042/224] create a pdf modal --- src/components/modals/FilePreviewModal.astro | 257 +++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/components/modals/FilePreviewModal.astro diff --git a/src/components/modals/FilePreviewModal.astro b/src/components/modals/FilePreviewModal.astro new file mode 100644 index 0000000..4ec963e --- /dev/null +++ b/src/components/modals/FilePreviewModal.astro @@ -0,0 +1,257 @@ +--- +interface Props { + id?: string; + title?: string; + className?: string; +} + +const { + id = "filePreviewModal", + title = "File Preview", + className = "", +} = Astro.props; +--- + + + + + + + + + + + + + + From 22dcb0fe625d486f08baf43ce232709d8e55db83 Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 03:25:45 -0800 Subject: [PATCH 043/224] fix the resume preview --- src/components/profile/UserSettings.astro | 94 ++++++++--------------- 1 file changed, 32 insertions(+), 62 deletions(-) diff --git a/src/components/profile/UserSettings.astro b/src/components/profile/UserSettings.astro index 49598a1..9f2a7b1 100644 --- a/src/components/profile/UserSettings.astro +++ b/src/components/profile/UserSettings.astro @@ -1,6 +1,8 @@ --- -// Import the majors list +// Import the majors list and FilePreviewModal import allMajors from "../../data/allUCSDMajors.txt?raw"; +import FilePreviewModal from "../modals/FilePreviewModal.astro"; + const majorsList: string[] = allMajors .split("\n") .filter((major: string) => major.trim()) @@ -226,49 +228,8 @@ const majorsList: string[] = allMajors
- - - - - + + From b811bb6545b6979f1614b240e59119f12e7a4a59 Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 04:13:48 -0800 Subject: [PATCH 046/224] partial working --- src/components/modals/FilePreviewModal.astro | 133 +++++++++++++++---- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/src/components/modals/FilePreviewModal.astro b/src/components/modals/FilePreviewModal.astro index 0e639d0..69475d6 100644 --- a/src/components/modals/FilePreviewModal.astro +++ b/src/components/modals/FilePreviewModal.astro @@ -399,24 +399,24 @@ const { } createFileCard(file) { - const fileType = this.getFileType(file.name); const card = document.createElement("div"); card.className = "card bg-base-200 hover:bg-base-300 cursor-pointer transition-colors"; + const fileType = this.getFileType(file.name); let preview = ""; + if (fileType === "image") { preview = `
- ${file.name} + ${file.name}
`; } else { + const icon = this.getFileTypeIcon(fileType); preview = `
- - - + ${icon}
`; } @@ -427,40 +427,125 @@ const {

${file.name}

+

${this.getFileTypeLabel(fileType)}

`; - card.addEventListener("click", () => this.showFile(file)); return card; } + getFileTypeLabel(fileType) { + switch (fileType) { + case "image": + return "Image"; + case "video": + return "Video"; + case "audio": + return "Audio"; + case "pdf": + return "PDF Document"; + case "word": + return "Word Document"; + case "excel": + return "Excel Spreadsheet"; + case "powerpoint": + return "PowerPoint Presentation"; + case "text": + return "Text Document"; + case "code": + return "Code File"; + case "archive": + return "Archive"; + default: + return "File"; + } + } + async show(files) { this.modal.showModal(); - if (!Array.isArray(files)) { - // Single file - await this.showFile(files); - } else if (files.length === 1) { - // Single file in array - await this.showFile(files[0]); - } else { - // Multiple files - this.singleView.classList.add("hidden"); - this.multipleView.classList.remove("hidden"); + // Show loading state immediately + this.showLoading(false); + this.showLoading(true); - // Show loading state - this.showLoading(true); + try { + // Normalize input to array + const fileArray = Array.isArray(files) ? files : [files]; + + // Handle empty files + if (fileArray.length === 0) { + this.previewContainer.innerHTML = ` +
+
+

No files available

+
+
+ `; + this.hideLoading(false); + return; + } + + // Single file view + if (fileArray.length === 1) { + this.singleView.classList.remove("hidden"); + this.multipleView.classList.add("hidden"); + await this.showFile(fileArray[0]); + } + // Multiple files view + else { + this.singleView.classList.add("hidden"); + this.multipleView.classList.remove("hidden"); - try { // Clear and populate file grid this.fileGrid.innerHTML = ""; - files.forEach((file) => { - this.fileGrid.appendChild(this.createFileCard(file)); + + // Create all file cards and handle their loading + const loadingPromises = fileArray.map(async (file) => { + const card = this.createFileCard(file); + this.fileGrid.appendChild(card); + + // If it's an image, wait for it to load + if (this.getFileType(file.name) === "image") { + const img = card.querySelector("img"); + if (img) { + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + // Add a timeout to prevent infinite loading + setTimeout(resolve, 5000); // 5 second timeout + }).catch(console.warn); // Log but don't fail if image fails to load + } + } + return card; + }); + + // Wait for all cards to be processed + await Promise.all(loadingPromises); + + // Add click handlers after all cards are loaded + const cards = this.fileGrid.querySelectorAll(".card"); + cards.forEach((card, index) => { + card.addEventListener("click", () => { + this.showFile(fileArray[index]); + this.singleView.classList.remove("hidden"); + this.multipleView.classList.add("hidden"); + }); }); - } finally { - // Hide loading state - this.hideLoading(true); } + } catch (error) { + console.error("Error showing files:", error); + this.previewContainer.innerHTML = ` +
+
+

Failed to load files

+

${error instanceof Error ? error.message : "Unknown error"}

+
+
+ `; + } finally { + // Hide both loading states + this.hideLoading(false); + this.hideLoading(true); } } From 1aa4c5e6ae214d7f99294dda0327d738e70efe0a Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 04:20:31 -0800 Subject: [PATCH 047/224] new file previewer modal old one is reusing the component, making lots of bugs appear --- src/components/modals/FileViewerModal.tsx | 379 ++++++++++++++++++ .../profile/DefaultProfileView.astro | 222 ++++------ 2 files changed, 446 insertions(+), 155 deletions(-) create mode 100644 src/components/modals/FileViewerModal.tsx diff --git a/src/components/modals/FileViewerModal.tsx b/src/components/modals/FileViewerModal.tsx new file mode 100644 index 0000000..b5c0fa2 --- /dev/null +++ b/src/components/modals/FileViewerModal.tsx @@ -0,0 +1,379 @@ +import React, { useState, useEffect } from 'react'; + +interface FileType { + url: string; + type: string; + name: string; +} + +interface FileViewerModalProps { + isOpen: boolean; + onClose: () => void; + files: FileType | FileType[]; + modalId?: string; +} + +// Create a wrapper component that listens to custom events +export const FileViewerModalWrapper: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const [files, setFiles] = useState([]); + + useEffect(() => { + // Listen for custom events to open/close modal and set files + const handleShowFiles = (event: CustomEvent) => { + const { files } = event.detail; + setFiles(Array.isArray(files) ? files : [files]); + setIsOpen(true); + }; + + // Add event listeners + window.addEventListener('showFileViewer' as any, handleShowFiles); + + // Cleanup + return () => { + window.removeEventListener('showFileViewer' as any, handleShowFiles); + }; + }, []); + + const handleClose = () => { + setIsOpen(false); + }; + + return ( + + ); +}; + +const FileViewerModal: React.FC = ({ isOpen, onClose, files, modalId = 'file-viewer' }) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [showPreview, setShowPreview] = useState(false); + + const fileArray = Array.isArray(files) ? files : [files]; + + // Helper function to check if file type is previewable + const isPreviewableType = (fileType: string): boolean => { + return ( + fileType.startsWith('image/') || + fileType.startsWith('video/') || + fileType.startsWith('audio/') || + fileType === 'application/pdf' || + fileType.startsWith('text/') || + fileType === 'application/json' + ); + }; + + useEffect(() => { + if (isOpen) { + // Only set loading if the file is previewable + if (selectedFile && isPreviewableType(selectedFile.type)) { + setLoading(true); + } else { + setLoading(false); + } + setError(null); + // If single file, show preview directly + if (!Array.isArray(files)) { + setSelectedFile(files); + setShowPreview(true); + } else { + setShowPreview(false); + setSelectedFile(null); + } + } + }, [isOpen, files]); + + const handleLoadSuccess = () => { + setLoading(false); + }; + + const handleLoadError = () => { + setLoading(false); + setError('Failed to load file'); + }; + + const handleFileSelect = (file: FileType) => { + setSelectedFile(file); + setShowPreview(true); + // Only set loading if the file is previewable + setLoading(isPreviewableType(file.type)); + setError(null); + }; + + const handleBackToList = () => { + setShowPreview(false); + setSelectedFile(null); + }; + + const renderFileContent = (file: FileType) => { + const fileType = file.type.toLowerCase(); + + // If not a previewable type, don't show loading state + if (!isPreviewableType(fileType)) { + return ( +
+
📄
+

+ This file type ({file.type}) cannot be previewed. +
+ + Open in New Tab + +

+
+ ); + } + + if (fileType.startsWith('image/')) { + return ( + {file.name} + ); + } + + if (fileType.startsWith('video/')) { + return ( + + ); + } + + if (fileType === 'application/pdf') { + return ( + -
-
- - - From 3181b0179d79305137a4d50abf1c3a94a552b5aa Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 13:04:43 -0800 Subject: [PATCH 054/224] fixed some bugs --- bun.lock | 2 - package-lock.json | 256 +++-- package.json | 10 +- src/components/modals/FilePreviewModal.astro | 568 ----------- .../profile/DefaultProfileView.astro | 934 +++++++++--------- src/pages/profile.astro | 671 ++++++------- 6 files changed, 967 insertions(+), 1474 deletions(-) delete mode 100644 src/components/modals/FilePreviewModal.astro diff --git a/bun.lock b/bun.lock index 8bdbc34..3090a6d 100644 --- a/bun.lock +++ b/bun.lock @@ -18,8 +18,6 @@ "motion": "^11.15.0", "next": "^15.1.2", "pocketbase": "^0.25.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", "react-icons": "^5.4.0", "tailwindcss": "^3.4.16", }, diff --git a/package-lock.json b/package-lock.json index f3d184f..e614c4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,21 +10,26 @@ "dependencies": { "@astrojs/mdx": "4.0.3", "@astrojs/node": "^9.0.0", - "@astrojs/react": "4.1.2", + "@astrojs/react": "^4.2.0", "@astrojs/tailwind": "5.1.4", - "@types/react": "^18.3.14", - "@types/react-dom": "^18.3.2", + "@types/js-yaml": "^4.0.9", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "astro": "5.1.1", "astro-expressive-code": "^0.38.3", "astro-icon": "^1.1.4", + "js-yaml": "^4.1.0", + "jszip": "^3.10.1", "motion": "^11.15.0", "next": "^15.1.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "pocketbase": "^0.25.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-icons": "^5.4.0", "tailwindcss": "^3.4.16" }, "devDependencies": { + "daisyui": "^4.12.23", "prettier": "^3.4.2", "prettier-plugin-astro": "^0.14.1", "tailwindcss-animated": "^1.1.2", @@ -170,14 +175,13 @@ } }, "node_modules/@astrojs/react": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-4.1.2.tgz", - "integrity": "sha512-Slw8Bho50w1+rYnSnDl5PDAUikSOEItx5DEJU5OgmarTirBr1audIb2DgC8faAGcGkq5WhuUVsSiq/TmSORlwA==", - "license": "MIT", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-4.2.0.tgz", + "integrity": "sha512-2OccnYFK+mLuy9GpJqPM3BQGvvemnXNeww+nBVYFuiH04L7YIdfg4Gq0LT7v/BraiuADV5uTl9VhTDL/ZQPAhw==", "dependencies": { "@vitejs/plugin-react": "^4.3.4", "ultrahtml": "^1.5.3", - "vite": "^6.0.5" + "vite": "^6.0.9" }, "engines": { "node": "^18.17.1 || ^20.3.0 || >=22.0.0" @@ -2137,6 +2141,11 @@ "@types/unist": "*" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2176,29 +2185,20 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", - "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "license": "MIT", + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", + "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", - "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", - "license": "MIT", + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", + "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.0.0" } }, "node_modules/@types/tar": { @@ -3085,6 +3085,11 @@ "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3140,6 +3145,16 @@ ], "license": "MIT" }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -3216,6 +3231,34 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.23.tgz", + "integrity": "sha512-EM38duvxutJ5PD65lO/AFMpcw+9qEy6XAZrTpzp7WyaPeO/l+F/Qiq0ECHHmFNcFXh5aVoALY4MGrrxtCiaQCQ==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3783,6 +3826,12 @@ "node": ">=8.6.0" } }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -3921,13 +3970,12 @@ } }, "node_modules/framer-motion": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", - "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", - "license": "MIT", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "dependencies": { - "motion-dom": "^11.14.3", - "motion-utils": "^11.14.3", + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { @@ -4506,6 +4554,11 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -4714,6 +4767,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4786,6 +4844,17 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -4801,6 +4870,14 @@ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "license": "MIT" }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -4903,18 +4980,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6166,16 +6231,17 @@ } }, "node_modules/motion-dom": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", - "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==", - "license": "MIT" + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "dependencies": { + "motion-utils": "^11.18.1" + } }, "node_modules/motion-utils": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", - "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==", - "license": "MIT" + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==" }, "node_modules/mrmime": { "version": "2.0.0", @@ -6525,6 +6591,11 @@ "integrity": "sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==", "license": "MIT" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -6731,6 +6802,11 @@ "pathe": "^1.1.2" } }, + "node_modules/pocketbase": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.25.1.tgz", + "integrity": "sha512-2IH0KLI/qMNR/E17C7BGWX2FxW7Tead+igLHOWZ45P56v/NyVT18Jnmddeft+3qWWGL1Hog2F8bc4orWV/+Fcg==" + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -6928,6 +7004,11 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7012,28 +7093,22 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.25.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.0.0" } }, "node_modules/react-icons": { @@ -7063,6 +7138,20 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7515,6 +7604,11 @@ "dev": true, "license": "MIT" }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7532,13 +7626,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" }, "node_modules/semver": { "version": "6.3.1", @@ -7578,6 +7668,11 @@ "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", "license": "ISC" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7753,6 +7848,14 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -8587,10 +8690,9 @@ } }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", - "license": "MIT", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.4.49", diff --git a/package.json b/package.json index 3fc19bd..cb396f0 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ "dependencies": { "@astrojs/mdx": "4.0.3", "@astrojs/node": "^9.0.0", - "@astrojs/react": "4.1.2", + "@astrojs/react": "^4.2.0", "@astrojs/tailwind": "5.1.4", "@types/js-yaml": "^4.0.9", - "@types/react": "^18.3.14", - "@types/react-dom": "^18.3.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "astro": "5.1.1", "astro-expressive-code": "^0.38.3", "astro-icon": "^1.1.4", @@ -25,8 +25,8 @@ "motion": "^11.15.0", "next": "^15.1.2", "pocketbase": "^0.25.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-icons": "^5.4.0", "tailwindcss": "^3.4.16" }, diff --git a/src/components/modals/FilePreviewModal.astro b/src/components/modals/FilePreviewModal.astro deleted file mode 100644 index 69475d6..0000000 --- a/src/components/modals/FilePreviewModal.astro +++ /dev/null @@ -1,568 +0,0 @@ ---- -interface Props { - id?: string; - title?: string; - className?: string; -} - -const { - id = "filePreviewModal", - title = "File Preview", - className = "", -} = Astro.props; ---- - - - - - - - - - - - diff --git a/src/components/profile/DefaultProfileView.astro b/src/components/profile/DefaultProfileView.astro index 3a17585..12a44e0 100644 --- a/src/components/profile/DefaultProfileView.astro +++ b/src/components/profile/DefaultProfileView.astro @@ -5,308 +5,298 @@ const {} = Astro.props; ---
- -
-
- -
-
-

Events

+ +
+
+ +
+
+

Events

+
+ + +
+
+

Quick Check-in

+ + +
+
+
+
+
+
+
+ + + +
+
- -
-
-

Quick Check-in

+
+
- -
-
-
-
-
-
-
-
-
- - -
diff --git a/src/components/dashboard/Officer_EventManagement/Attendees.tsx b/src/components/dashboard/Officer_EventManagement/Attendees.tsx index 2deb387..03f80e6 100644 --- a/src/components/dashboard/Officer_EventManagement/Attendees.tsx +++ b/src/components/dashboard/Officer_EventManagement/Attendees.tsx @@ -232,7 +232,6 @@ export default function Attendees() { let isMounted = true; const fetchEventData = async () => { if (!eventId || !auth.isAuthenticated()) { - if (!eventId) console.log('No eventId provided'); if (!auth.isAuthenticated()) { console.log('User not authenticated'); setError('Authentication required'); From 5aecaa83703216d2902d493773e8fd3919a2ae93 Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 18 Feb 2025 03:35:07 -0800 Subject: [PATCH 139/224] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 016b59e..029ead8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ # generated types .astro/ +.cursor # dependencies node_modules/ From bb2f3f93d3582567e241ec620938a387b362eeb0 Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 18 Feb 2025 04:05:08 -0800 Subject: [PATCH 140/224] remove unused imports --- src/components/dashboard/universal/FilePreview.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/dashboard/universal/FilePreview.tsx b/src/components/dashboard/universal/FilePreview.tsx index c56cd48..8f5fafd 100644 --- a/src/components/dashboard/universal/FilePreview.tsx +++ b/src/components/dashboard/universal/FilePreview.tsx @@ -21,7 +21,6 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil const [loading, setLoading] = useState(false); const [fileType, setFileType] = useState(null); const [isVisible, setIsVisible] = useState(false); - const [isExpanded, setIsExpanded] = useState(false); const [visibleLines, setVisibleLines] = useState(20); const CHUNK_SIZE = 50; // Number of additional lines to show when expanding const INITIAL_LINES_TO_SHOW = 20; From b5cdc7464b46a53de245bd02b33b40273f1a8d1b Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 18 Feb 2025 04:05:19 -0800 Subject: [PATCH 141/224] separated stats --- src/components/dashboard/ProfileSection.astro | 62 +-------- .../dashboard/ProfileSection/Stats.tsx | 123 ++++++++++++++++++ 2 files changed, 125 insertions(+), 60 deletions(-) create mode 100644 src/components/dashboard/ProfileSection/Stats.tsx diff --git a/src/components/dashboard/ProfileSection.astro b/src/components/dashboard/ProfileSection.astro index 088726b..7124c78 100644 --- a/src/components/dashboard/ProfileSection.astro +++ b/src/components/dashboard/ProfileSection.astro @@ -1,6 +1,7 @@ --- import { Icon } from "astro-icon/components"; import ShowProfileLogs from "./ProfileSection/ShowProfileLogs"; +import { Stats } from "./ProfileSection/Stats"; ---
@@ -9,66 +10,7 @@ import ShowProfileLogs from "./ProfileSection/ShowProfileLogs";

Welcome to your IEEE UCSD dashboard

- -
-
-
-
- Events Attended -
-
- 0 -
-
-
- Since joining -
-
-
-
-
-
-
- Loyalty Points -
-
- 0 -
-
-
- No activity -
-
-
-
-
-
-
- Activity Level -
-
- Low -
-
-
- New Member -
-
-
-
-
+
; +} + +export function Stats() { + const [eventsAttended, setEventsAttended] = useState(0); + const [loyaltyPoints, setLoyaltyPoints] = useState(0); + const [activityLevel, setActivityLevel] = useState("Low"); + const [activityDesc, setActivityDesc] = useState("New Member"); + const [pointsChange, setPointsChange] = useState("No activity"); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchStats = async () => { + try { + setIsLoading(true); + const get = Get.getInstance(); + const auth = Authentication.getInstance(); + const userId = auth.getCurrentUser()?.id; + + if (!userId) return; + + // Fetch all events + const events = await get.getAll("events"); + + // Count events where user is in attendees + const attendedEvents = events.filter(event => + event.attendees?.some(attendee => attendee.user_id === userId) + ); + + const numEventsAttended = attendedEvents.length; + setEventsAttended(numEventsAttended); + + // Calculate loyalty points (1 point per event) + const points = numEventsAttended; + setLoyaltyPoints(points); + + // Set points change message + if (points > 0) { + setPointsChange(`+${points} this semester`); + } + + // Determine activity level + if (points >= 10) { + setActivityLevel("High"); + setActivityDesc("Very Active"); + } else if (points >= 5) { + setActivityLevel("Medium"); + setActivityDesc("Active Member"); + } else if (points >= 1) { + setActivityLevel("Low"); + setActivityDesc("Getting Started"); + } + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + setIsLoading(false); + } + }; + + fetchStats(); + }, []); + + if (isLoading) { + return ( +
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ); + } + + return ( +
+
+
+
Events Attended
+
{eventsAttended}
+
+
Since joining
+
+
+
+
+
+
Loyalty Points
+
{loyaltyPoints}
+
+
{pointsChange}
+
+
+
+
+
+
Activity Level
+
{activityLevel}
+
+
{activityDesc}
+
+
+
+
+ ); +} From 45b278a2fcf21a3644c350edbb59afae2fb1c48c Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 18 Feb 2025 04:35:41 -0800 Subject: [PATCH 142/224] add dynamic view --- src/components/dashboard/EventsSection.astro | 117 +++++--- .../dashboard/EventsSection/EventCheckIn.tsx | 32 +-- .../dashboard/EventsSection/EventLoad.tsx | 119 ++++---- .../dashboard/Officer_EventManagement.astro | 255 +++++++++++------- src/pages/dashboard.astro | 127 ++++++--- 5 files changed, 392 insertions(+), 258 deletions(-) diff --git a/src/components/dashboard/EventsSection.astro b/src/components/dashboard/EventsSection.astro index f6ed2cf..7598bd2 100644 --- a/src/components/dashboard/EventsSection.astro +++ b/src/components/dashboard/EventsSection.astro @@ -7,43 +7,63 @@ import EventLoad from "./EventsSection/EventLoad"; ---