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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
User Profile
+
+
+
+
+
+
+ Not signed in
+
+
+
+
+
+
+ Not signed in
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Never
+
+
+
+
+
+
+
+
+ No resume uploaded
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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}
+
{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";