Add online store

This commit is contained in:
chark1es 2025-01-24 04:00:29 -08:00
parent ea8c16747d
commit 72cb6ecb49
11 changed files with 797 additions and 7 deletions

View file

@ -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=="],

View file

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

View file

@ -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 = `<p class='text-red-500'>${message}</p>`;
}
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 = `
<p class="text-3xl font-bold text-green-500 mb-4">Authentication Successful!</p>
<p class="text-2xl font-medium">Redirecting to store...</p>
<div class="mt-4">
<div class="loading loading-spinner loading-lg"></div>
</div>
`;
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}`);
}
}
}

View file

@ -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();
});
}
}

View file

@ -0,0 +1,193 @@
<div>
<!-- Loading Skeleton (shown by default) -->
<div id="loadingSkeleton" class="card bg-base-200 shadow-xl">
<div class="card-body p-0">
<div class="px-6 pt-6">
<!-- Title -->
<h2 class="skeleton h-8 w-40 card-title"></h2>
</div>
<!-- Content -->
<div class="px-6 pb-6">
<div class="space-y-3">
<!-- Name -->
<div class="space-y-1">
<div class="skeleton h-3 w-16 opacity-70"></div>
<div class="skeleton h-[1.75rem] w-48"></div>
</div>
<div class="divider my-0.5"></div>
<!-- Email -->
<div class="space-y-1">
<div class="skeleton h-3 w-16 opacity-70"></div>
<div class="skeleton h-[1.75rem] w-64"></div>
</div>
<div class="divider my-0.5"></div>
<!-- Member Status -->
<div class="space-y-1">
<div class="skeleton h-3 w-24 opacity-70"></div>
<div class="skeleton h-[1.75rem] w-32"></div>
</div>
<div class="divider my-0.5"></div>
<!-- Member ID -->
<div class="space-y-1">
<div class="skeleton h-3 w-20 opacity-70"></div>
<div class="flex items-center gap-2">
<div class="skeleton h-8 flex-1"></div>
<div class="skeleton h-8 w-16"></div>
</div>
</div>
<div class="divider my-0.5"></div>
<!-- Last Login -->
<div class="space-y-1">
<div class="skeleton h-3 w-20 opacity-70"></div>
<div class="skeleton h-[1.25rem] w-32"></div>
</div>
<div class="divider my-0.5"></div>
<!-- Resume -->
<div class="space-y-1">
<div class="skeleton h-3 w-16 opacity-70"></div>
<div class="space-y-2">
<div class="flex items-center gap-2">
<div class="skeleton h-[1.25rem] flex-1"></div>
<div class="skeleton h-[1.25rem] w-24"></div>
</div>
<div class="skeleton h-8 w-full"></div>
</div>
</div>
<div class="divider my-0.5"></div>
<!-- Auth Button -->
<div class="pt-2">
<div class="skeleton h-10 w-full"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Actual Content (hidden by default) -->
<div id="userInfo" class="card bg-base-200 shadow-xl opacity-0 hidden">
<div class="card-body p-0">
<div class="px-6 pt-6">
<h2 class="card-title text-2xl">User Profile</h2>
</div>
<div class="px-6 pb-6">
<div class="space-y-3">
<div class="space-y-1">
<label class="text-sm opacity-70">Name</label>
<p id="userName" class="h-[1.75rem] font-medium">
Not signed in
</p>
</div>
<div class="divider my-0.5"></div>
<div class="space-y-1">
<label class="text-sm opacity-70">Email</label>
<p id="userEmail" class="h-[1.75rem] font-medium">
Not signed in
</p>
</div>
<div class="divider my-0.5"></div>
<div class="space-y-1">
<label class="text-sm opacity-70">Member Status</label>
<div class="flex h-[1.75rem] items-center">
<div id="memberStatus" class="badge badge-neutral">
Not verified
</div>
</div>
</div>
<div class="divider my-0.5"></div>
<div class="space-y-1">
<label class="text-sm opacity-70">Member ID</label>
<div class="flex items-center gap-2 h-8">
<input
type="text"
id="memberIdInput"
placeholder="Enter your member ID"
class="input input-bordered w-full h-8 min-h-[2rem] disabled:bg-base-300 disabled:border-2 disabled:border-opacity-50 disabled:cursor-not-allowed"
/>
<button
id="saveMemberId"
class="btn btn-primary h-8 min-h-[2rem]"
>Save</button
>
</div>
<p id="memberIdStatus" class="text-xs mt-1 opacity-70">
</p>
</div>
<div class="divider my-0.5"></div>
<div class="space-y-1">
<label class="text-sm opacity-70">Last Login</label>
<p
id="lastLogin"
class="text-sm h-[1.25rem] opacity-80"
>
Never
</p>
</div>
<div class="divider my-0.5"></div>
<div class="space-y-2">
<label class="text-sm opacity-70">Resume</label>
<div id="resumeSection" class="space-y-2">
<div class="flex items-center gap-2 h-[1.25rem]">
<p
id="resumeName"
class="text-sm truncate flex-1"
>
No resume uploaded
</p>
<div id="resumeActions" class="flex gap-2">
<a
id="resumeDownload"
href="#"
target="_blank"
class="btn btn-ghost btn-xs">View</a
>
<button
id="deleteResume"
class="btn btn-ghost btn-xs text-error"
>Delete</button
>
</div>
</div>
<div id="uploadSection">
<input
type="file"
id="resumeUpload"
accept=".pdf,.doc,.docx"
class="file-input file-input-bordered file-input-sm w-full"
/>
<p
id="uploadStatus"
class="text-xs mt-1 opacity-70"
>
</p>
</div>
</div>
</div>
<div class="divider my-0.5"></div>
<div class="pt-2">
<button id="loginButton" class="btn btn-primary w-full"
>Sign in with IEEEUCSD SSO</button
>
<button id="logoutButton" class="btn btn-error w-full"
>Sign Out</button
>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.hidden {
display: none;
}
#userInfo {
transition: opacity 0.3s ease-in-out;
}
</style>

View file

@ -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;
---
<div class="card bg-base-200 shadow-xl">
<figure class="px-6 pt-6">
<div class="relative w-full">
<div class="skeleton w-full aspect-[4/3] rounded-xl"></div>
<img
src={imageUrl}
alt={name}
class="rounded-xl absolute inset-0 w-full h-full object-cover opacity-0 transition-opacity duration-300"
onload="this.classList.remove('opacity-0')"
/>
</div>
</figure>
<div class="card-body">
<h2 class="card-title">{name}</h2>
<p>{description}</p>
<div class="card-actions justify-between items-center mt-2">
<span class="text-xl font-semibold">${price.toFixed(2)}</span>
<button class="btn btn-primary">Add to Cart</button>
</div>
</div>
</div>

View file

@ -14,7 +14,9 @@ import InView from "../components/core/InView.astro";
<title>Astro Basics</title>
</head>
<InView />
<body class="text-white bg-ieee-black relative flex flex-col items-center min-h-screen justify-between overflow-x-clip">
<body
class="text-white bg-ieee-black relative flex flex-col items-center min-h-screen justify-between overflow-x-clip"
>
<Navbar />
<main class="w-[95%]">
<slot />
@ -23,6 +25,9 @@ import InView from "../components/core/InView.astro";
</body>
<style>
:root {
background-color: rgb(10 14 26 / var(--tw-bg-opacity, 1));
}
html,
body {
margin: 0;

View file

@ -0,0 +1,20 @@
---
import Layout from "../layouts/Layout.astro";
const title = "Authenticating...";
---
<Layout {title}>
<main class="min-h-screen flex items-center justify-center">
<div id="content" class="text-center">
<p class="text-2xl font-medium">Redirecting to store...</p>
<div class="mt-4">
<div class="loading loading-spinner loading-lg"></div>
</div>
</div>
</main>
</Layout>
<script>
import { RedirectHandler } from "../components/auth/RedirectHandler";
new RedirectHandler();
</script>

View file

@ -0,0 +1,51 @@
---
import Layout from "../layouts/Layout.astro";
import UserProfile from "../components/auth/UserProfile.astro";
import StoreItem from "../components/store/StoreItem.astro";
const title = "IEEE Store";
---
<Layout {title}>
<main class="w-[95%] mx-auto pb-12 md:pt-[14vh] pt-[12vw] min-h-screen">
<h1 class="text-4xl font-bold mb-12">IEEE UCSD Store</h1>
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Left Column - User Info -->
<div class="md:col-span-1 h-fit">
<UserProfile />
</div>
<!-- Right Column - Store Items -->
<div id="storeContent" class="md:col-span-3">
<div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
>
<StoreItem
name="Item Name"
description="Description of the item goes here. This is a placeholder."
price={20.0}
/>
<StoreItem
name="Item Name"
description="Description of the item goes here. This is a placeholder."
price={25.0}
/>
<StoreItem
name="Item Name"
description="Description of the item goes here. This is a placeholder."
price={15.0}
/>
<StoreItem
name="Item Name"
description="Description of the item goes here. This is a placeholder."
price={15.0}
/>
</div>
</div>
</div>
</main>
</Layout>
<script>
import { StoreAuth } from "../components/auth/StoreAuth";
new StoreAuth();
</script>

View file

@ -9,7 +9,8 @@ export default {
"animate-delay-100",
"animate-delay-300",
"animate-delay-500",
"animate-delay-700"],
"animate-delay-700",
],
theme: {
extend: {
boxShadow: {
@ -35,7 +36,9 @@ export default {
plugins: [
require("tailwindcss-motion"),
require("tailwindcss-animated"),
require("daisyui"),
function ({ addVariant }) {
addVariant("in-view", "&.in-view");
},],
},
],
};