diff --git a/src/components/dashboard/SettingsSection/UserProfileSettings.tsx b/src/components/dashboard/SettingsSection/UserProfileSettings.tsx index 1ad1487..ae9faaa 100644 --- a/src/components/dashboard/SettingsSection/UserProfileSettings.tsx +++ b/src/components/dashboard/SettingsSection/UserProfileSettings.tsx @@ -35,12 +35,6 @@ export default function UserProfileSettings({ // Use environment variables or props (fallback) const logtoApiEndpoint = envLogtoApiEndpoint || propLogtoApiEndpoint; - // Parse the majors list from the text file and sort alphabetically - const majorsList = allMajors - .split('\n') - .filter(major => major.trim() !== '') - .sort((a, b) => a.localeCompare(b)); - useEffect(() => { const loadUserData = async () => { try { @@ -83,6 +77,7 @@ export default function UserProfileSettings({ // Extract username from Logto data or email if not set const defaultUsername = logtoUser.data?.username || currentUser.email?.split('@')[0] || ''; + // Remove all the major matching logic and just use the server value directly setUser(currentUser); setFormData({ name: currentUser.name || '', @@ -334,11 +329,23 @@ export default function UserProfileSettings({ className="select select-bordered w-full" > - {majorsList.map((major, index) => ( - - ))} + {(() => { + const standardMajors = allMajors + .split('\n') + .filter(major => major.trim() !== '') + .sort((a, b) => a.localeCompare(b)); + + if (formData.major && !standardMajors.includes(formData.major)) { + standardMajors.push(formData.major); + standardMajors.sort((a, b) => a.localeCompare(b)); + } + + return standardMajors.map((major, index) => ( + + )); + })()} diff --git a/src/components/dashboard/universal/DashboardWrapper.tsx b/src/components/dashboard/universal/DashboardWrapper.tsx new file mode 100644 index 0000000..f49a550 --- /dev/null +++ b/src/components/dashboard/universal/DashboardWrapper.tsx @@ -0,0 +1,67 @@ +import { useState, useEffect } from "react"; +import type { ReactNode } from "react"; +import { Authentication } from "../../../scripts/pocketbase/Authentication"; +import type { User } from "../../../schemas/pocketbase/schema"; +import FirstTimeLoginPopup from "./FirstTimeLoginPopup"; + +interface DashboardWrapperProps { + children: ReactNode; + logtoApiEndpoint?: string; +} + +const DashboardWrapper = ({ children, logtoApiEndpoint }: DashboardWrapperProps) => { + const [showOnboarding, setShowOnboarding] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const checkUserStatus = async () => { + try { + const auth = Authentication.getInstance(); + if (!auth.isAuthenticated()) { + // Not logged in, so don't show onboarding + setIsLoading(false); + return; + } + + const userData = auth.getCurrentUser() as User | null; + + if (userData) { + // If signed_up is explicitly false, show onboarding + setShowOnboarding(userData.signed_up === false); + } + } catch (error) { + console.error("Error checking user status:", error); + } finally { + setIsLoading(false); + } + }; + + checkUserStatus(); + }, []); + + const handleOnboardingComplete = () => { + setShowOnboarding(false); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( + <> + {showOnboarding && ( + + )} + {children} + + ); +}; + +export default DashboardWrapper; \ No newline at end of file diff --git a/src/components/dashboard/universal/FirstTimeLoginManager.tsx b/src/components/dashboard/universal/FirstTimeLoginManager.tsx new file mode 100644 index 0000000..2c4a1e8 --- /dev/null +++ b/src/components/dashboard/universal/FirstTimeLoginManager.tsx @@ -0,0 +1,56 @@ +import { useState, useEffect } from "react"; +import { Authentication } from "../../../scripts/pocketbase/Authentication"; +import type { User } from "../../../schemas/pocketbase/schema"; +import FirstTimeLoginPopup from "./FirstTimeLoginPopup"; + +interface FirstTimeLoginManagerProps { + logtoApiEndpoint?: string; +} + +const FirstTimeLoginManager = ({ logtoApiEndpoint }: FirstTimeLoginManagerProps) => { + const [showOnboarding, setShowOnboarding] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const checkUserStatus = async () => { + try { + const auth = Authentication.getInstance(); + if (!auth.isAuthenticated()) { + // Not logged in, so don't show onboarding + setIsLoading(false); + return; + } + + // Using the new method to check if user has signed up + const isSignedUp = auth.isUserSignedUp(); + console.log("User signed up status:", isSignedUp); + + // If not signed up, show onboarding + setShowOnboarding(!isSignedUp); + } catch (error) { + console.error("Error checking user status:", error); + } finally { + setIsLoading(false); + } + }; + + checkUserStatus(); + }, []); + + const handleOnboardingComplete = () => { + setShowOnboarding(false); + }; + + if (isLoading || !showOnboarding) { + return null; + } + + return ( + + ); +}; + +export default FirstTimeLoginManager; \ No newline at end of file diff --git a/src/components/dashboard/universal/FirstTimeLoginPopup.tsx b/src/components/dashboard/universal/FirstTimeLoginPopup.tsx new file mode 100644 index 0000000..5e42feb --- /dev/null +++ b/src/components/dashboard/universal/FirstTimeLoginPopup.tsx @@ -0,0 +1,701 @@ +import { useState, useEffect } from "react"; +import { Icon } from "@iconify/react"; +import { Update } from "../../../scripts/pocketbase/Update"; +import { Authentication } from "../../../scripts/pocketbase/Authentication"; +import type { User } from "../../../schemas/pocketbase/schema"; +import CustomAlert from "./CustomAlert"; +import { motion, AnimatePresence } from "framer-motion"; + +interface FirstTimeLoginPopupProps { + logtoApiEndpoint?: string; + onComplete?: () => void; +} + +const ucsdMajors = [ + "Aerospace Engineering", + "Aerospace Engineering – Aerothermodynamics", + "Aerospace Engineering – Astrodynamics and Space Applications", + "Aerospace Engineering – Flight Dynamics and Controls", + "Anthropology", + "Art History/Criticism", + "Astronomy & Astrophysics", + "Biochemistry", + "Biochemistry and Cell Biology", + "Biology with Specialization in Bioinformatics", + "Bioengineering", + "Business Economics", + "Business Psychology", + "Chemical Engineering", + "Chemistry", + "Chinese Studies", + "Cinematic Arts", + "Classical Studies", + "Cognitive Science", + "Cognitive Science – Clinical Aspects of Cognition", + "Cognitive Science – Design and Interaction", + "Cognitive Science – Language and Culture", + "Cognitive Science – Machine Learning and Neural Computation", + "Cognitive Science – Neuroscience", + "Cognitive and Behavioral Neuroscience", + "Communication", + "Computer Engineering", + "Computer Science", + "Computer Science – Bioinformatics", + "Critical Gender Studies", + "Dance", + "Data Science", + "Ecology, Behavior and Evolution", + "Economics", + "Economics and Mathematics – Joint Major", + "Economics-Public Policy", + "Education Sciences", + "Electrical Engineering", + "Electrical Engineering and Society", + "Engineering Physics", + "Environmental Chemistry", + "Environmental Systems (Earth Sciences)", + "Environmental Systems (Ecology, Behavior & Evolution)", + "Environmental Systems (Environmental Chemistry)", + "Environmental Systems (Environmental Policy)", + "Ethnic Studies", + "General Biology", + "General Physics", + "General Physics/Secondary Education", + "Geosciences", + "German Studies", + "Global Health", + "Global South Studies", + "History", + "Human Biology", + "Human Developmental Sciences", + "Human Developmental Sciences – Equity and Diversity", + "Human Developmental Sciences – Healthy Aging", + "Interdisciplinary Computing and the Arts", + "International Studies – Anthropology", + "International Studies – Economics", + "International Studies – Economics (Joint BA/MIA)", + "International Studies – History", + "International Studies – International Business", + "International Studies – International Business (Joint BA/MIA)", + "International Studies – Linguistics", + "International Studies – Literature", + "International Studies – Philosophy", + "International Studies – Political Science", + "International Studies – Political Science (Joint BA/MIA)", + "International Studies – Sociology", + "Italian Studies", + "Japanese Studies", + "Jewish Studies", + "Latin American Studies", + "Latin American Studies – Mexico", + "Latin American Studies – Migration and Border Studies", + "Linguistics", + "Linguistics – Cognition and Language", + "Linguistics – Language and Society", + "Linguistics – Speech and Language Sciences", + "Linguistics: Language Studies", + "Literary Arts", + "Literatures in English", + "Marine Biology", + "Mathematical Biology", + "Mathematics", + "Mathematics – Applied Science", + "Mathematics – Computer Science", + "Mathematics – Secondary Education", + "Mathematics (Applied)", + "Mechanical Engineering", + "Mechanical Engineering – Controls and Robotics", + "Mechanical Engineering – Fluid Mechanics and Thermal Systems", + "Mechanical Engineering – Materials Science and Engineering", + "Mechanical Engineering – Mechanics of Materials", + "Mechanical Engineering – Renewable Energy and Environmental Flows", + "Media", + "Media Industries and Communication", + "Microbiology", + "Molecular Synthesis", + "Molecular and Cell Biology", + "Music", + "Music Humanities", + "NanoEngineering", + "Neurobiology / Physiology and Neuroscience", + "Oceanic and Atmospheric Sciences", + "Pharmacological Chemistry", + "Philosophy", + "Physics", + "Physics – Astrophysics", + "Physics – Biophysics", + "Physics – Computational Physics", + "Physics – Earth Sciences", + "Physics – Materials Physics", + "Political Science", + "Political Science – American Politics", + "Political Science – Comparative Politics", + "Political Science – Data Analytics", + "Political Science – International Affairs", + "Political Science – International Relations", + "Political Science – Political Theory", + "Political Science – Public Law", + "Political Science – Public Policy", + "Political Science – Race, Ethnicity, and Politics", + "Probability and Statistics", + "Psychology", + "Psychology – Clinical Psychology", + "Psychology – Cognitive Psychology", + "Psychology – Developmental Psychology", + "Psychology – Human Health", + "Psychology – Sensation and Perception", + "Psychology – Social Psychology", + "Public Health", + "Public Health – Biostatistics", + "Public Health – Climate and Environmental Sciences", + "Public Health – Community Health Sciences", + "Public Health – Epidemiology", + "Public Health – Health Policy and Management Sciences", + "Public Health – Medicine Sciences", + "Real Estate and Development", + "Russian, East European & Eurasian Studies", + "Sociology", + "Sociology – American Studies", + "Sociology – Culture and Communication", + "Sociology – Economy and Society", + "Sociology – International Studies", + "Sociology – Law and Society", + "Sociology – Science and Medicine", + "Sociology – Social Inequality", + "Spanish Literature", + "Speculative Design", + "Structural Engineering", + "Structural Engineering – Aerospace Structures", + "Structural Engineering – Civil Structures", + "Structural Engineering – Geotechnical Engineering", + "Structural Engineering – Structural Health Monitoring/Non-Destructive Evaluation", + "Studio", + "Study of Religion", + "Theatre", + "Undeclared – Humanities/Arts", + "Undeclared – Physical Sciences", + "Undeclared – Social Sciences", + "Urban Studies and Planning", + "World Literature and Culture", + "Other" +].sort(); // Ensure alphabetical order + +// Animation variants +const overlayVariants = { + hidden: { opacity: 0 }, + visible: { opacity: 1, transition: { duration: 0.3 } } +}; + +const popupVariants = { + hidden: { opacity: 0, scale: 0.9, y: 20 }, + visible: { + opacity: 1, + scale: 1, + y: 0, + transition: { + type: "spring", + damping: 25, + stiffness: 300, + duration: 0.4 + } + }, + exit: { + opacity: 0, + scale: 0.95, + y: -10, + transition: { + duration: 0.2 + } + } +}; + +const formItemVariants = { + hidden: { opacity: 0, y: 10 }, + visible: (i: number) => ({ + opacity: 1, + y: 0, + transition: { + delay: i * 0.1, + duration: 0.3 + } + }) +}; + +const FirstTimeLoginPopup = ({ logtoApiEndpoint, onComplete }: FirstTimeLoginPopupProps) => { + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [successMessage, setSuccessMessage] = useState(""); + + // Form state + const [formData, setFormData] = useState({ + name: "", + username: "", + pid: "", + member_id: "", // Optional + graduation_year: new Date().getFullYear() + 4, // Default to 4 years from now + major: "" + }); + + // Validation state + const [isValid, setIsValid] = useState({ + name: false, + username: false, + pid: false, + graduation_year: true, + major: false + }); + + // Get current user data + useEffect(() => { + const fetchUserData = async () => { + try { + const auth = Authentication.getInstance(); + const userData = auth.getCurrentUser() as User | null; + + if (userData) { + setFormData(prev => ({ + ...prev, + name: userData.name || "", + username: userData.username || "", + pid: userData.pid || "", + member_id: userData.member_id || "", + graduation_year: userData.graduation_year || new Date().getFullYear() + 4, + major: userData.major || "" + })); + + // Update validation state based on existing data + setIsValid({ + name: !!userData.name, + username: !!userData.username, + pid: !!userData.pid, + graduation_year: true, + major: !!userData.major + }); + } + } catch (error) { + console.error("Error fetching user data:", error); + setErrorMessage("Failed to load your profile information. Please try again."); + } finally { + setIsLoading(false); + } + }; + + fetchUserData(); + }, []); + + // Validate form + useEffect(() => { + setIsValid({ + name: formData.name.trim().length > 0, + username: /^[a-z0-9_]{3,20}$/.test(formData.username), + pid: /^[A-Za-z]\d{8}$/.test(formData.pid), + graduation_year: + !isNaN(parseInt(formData.graduation_year.toString())) && + parseInt(formData.graduation_year.toString()) >= new Date().getFullYear(), + major: formData.major !== "" // Check if a major is selected + }); + }, [formData]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: name === "graduation_year" ? parseInt(value) : value + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setErrorMessage(""); + setSuccessMessage(""); + + const allRequiredValid = isValid.name && isValid.username && isValid.pid && isValid.graduation_year && isValid.major; + + if (!allRequiredValid) { + setErrorMessage("Please fill in all required fields with valid information."); + return; + } + + setIsSaving(true); + + try { + // Update PocketBase user + const auth = Authentication.getInstance(); + const userId = auth.getUserId(); + + if (!userId) { + throw new Error("No user ID found. Please log in again."); + } + + const updateInstance = Update.getInstance(); + await updateInstance.updateFields("users", userId, { + name: formData.name, + username: formData.username, + pid: formData.pid, + member_id: formData.member_id || undefined, + graduation_year: formData.graduation_year, + major: formData.major, + signed_up: true // Set signed_up to true after completing onboarding + }); + + console.log("Saving first-time user data with signed_up=true"); + + // Update Logto user if endpoint is provided + if (logtoApiEndpoint) { + const accessToken = localStorage.getItem("access_token"); + if (accessToken) { + const response = await fetch("/api/update-logto-user", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: formData.name, + custom_data: { + username: formData.username, + pid: formData.pid, + member_id: formData.member_id || "", + graduation_year: formData.graduation_year, + major: formData.major, + signed_up: true + } + }), + }); + + if (!response.ok) { + throw new Error("Failed to update Logto user data"); + } + } + } + + console.log("Successfully updated PocketBase user with signed_up=true"); + + if (logtoApiEndpoint) { + console.log("Successfully updated Logto user profile with signed_up=true"); + } + + setSuccessMessage("Profile information saved successfully!"); + + // Call onComplete callback if provided + if (onComplete) { + setTimeout(() => { + onComplete(); + }, 1500); // Show success message briefly before completing + } + } catch (error) { + console.error("Error saving user data:", error); + // Check if the error might be related to username uniqueness + const errorMsg = error instanceof Error ? error.message : "Unknown error"; + if (errorMsg.toLowerCase().includes("username") || errorMsg.toLowerCase().includes("unique")) { + setErrorMessage("Failed to save your profile information. The username you chose might already be taken. Please try a different username."); + } else { + setErrorMessage("Failed to save your profile information. Please try again."); + } + } finally { + setIsSaving(false); + } + }; + + // Check if form can be submitted (all required fields valid) + const canSubmit = isValid.name && isValid.username && isValid.pid && isValid.graduation_year && isValid.major; + + return ( + + + +
+
+
+

Complete Your Profile

+
+ +
+
+

+ Welcome to IEEE UCSD! Please complete your profile to continue. +

+ + + +
+ + {isLoading ? ( +
+ +
+ ) : ( +
+
+ {/* Name Field */} + + + + {!isValid.name && formData.name && ( + + )} + + + {/* Username Field */} + + +
+ +
+ {!isValid.username && formData.username && ( + + )} + +
+ + {/* PID Field */} + + + + {!isValid.pid && formData.pid && ( + + )} + + + {/* Member ID Field (Optional) */} + + + + + + {/* Graduation Year Field */} + + + + {!isValid.graduation_year && ( + + )} + + + {/* Major Field */} + + + + {!isValid.major && formData.major && ( + + )} + + + {/* Error/Success Messages */} + + {errorMessage && ( + + + + )} + + {successMessage && ( + + + + )} + + + {/* Submit Button */} + + +

+ * Required fields +

+
+
+
+ )} +
+
+
+
+ ); +}; + +export default FirstTimeLoginPopup; \ No newline at end of file diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index c231635..65f8ee1 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -10,26 +10,30 @@ import { hasAccess, type OfficerStatus } from "../utils/roleAccess"; import { OfficerTypes } from "../schemas/pocketbase/schema"; import { initAuthSync } from "../scripts/database/initAuthSync"; import ToastProvider from "../components/dashboard/universal/ToastProvider"; +import FirstTimeLoginManager from "../components/dashboard/universal/FirstTimeLoginManager"; const title = "Dashboard"; +// Load environment variables +const logtoApiEndpoint = import.meta.env.LOGTO_API_ENDPOINT || ""; + // Load and parse dashboard config const configPath = path.join(process.cwd(), "src", "config", "dashboard.yaml"); const dashboardConfig = yaml.load(fs.readFileSync(configPath, "utf8")) as any; // Dynamically import all dashboard components const components = Object.fromEntries( - await Promise.all( - Object.values(dashboardConfig.sections) - .filter((section: any) => section.component) // Only process sections with components - .map(async (section: any) => { - const component = await import( - `../components/dashboard/${section.component}.astro` - ); - // console.log(`Loaded component: ${section.component}`); // Debug log - return [section.component, component.default]; - }), - ), + await Promise.all( + Object.values(dashboardConfig.sections) + .filter((section: any) => section.component) // Only process sections with components + .map(async (section: any) => { + const component = await import( + `../components/dashboard/${section.component}.astro` + ); + // console.log(`Loaded component: ${section.component}`); // Debug log + return [section.component, component.default]; + }) + ) ); // console.log("Available components:", Object.keys(components)); // Debug log @@ -37,769 +41,895 @@ const components = Object.fromEntries( - - - - {title} | IEEE UCSD - - - - -
- - - - -
- -
-
-
- -

IEEE UCSD

-
-
-
- - -
- -
-
-
-

Loading dashboard...

-
-
- - - - - - - - -
- { - Object.entries(dashboardConfig.sections).map( - ([sectionKey, section]: [string, any]) => { - // Skip if no component is defined - if (!section.component) return null; - - const Component = components[section.component]; - return ( - + + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +

IEEE UCSD

+
+
+
+ + +
+ +
+
+
+
+

Loading dashboard...

+
+
+ + + + + + + + +
+ { + Object.entries(dashboardConfig.sections).map( + ([sectionKey, section]: [string, any]) => { + // Skip if no component is defined + if (!section.component) return null; + + const Component = + components[section.component]; + return ( + + ); + } + ) + } +
+
+
-
-
- - + + - + ) } - // For non-sponsor roles, handle normally - document.querySelectorAll("[data-role-required]").forEach((element) => { - const requiredRole = element.getAttribute( - "data-role-required", - ) as OfficerStatus; + - + + diff --git a/src/scripts/checkFirstTimeUsers.ts b/src/scripts/checkFirstTimeUsers.ts new file mode 100644 index 0000000..451dfe5 --- /dev/null +++ b/src/scripts/checkFirstTimeUsers.ts @@ -0,0 +1,70 @@ +/** + * Utility script to check and set the signed_up field for users + * This can be run manually to test the first-time login functionality + */ + +import { Get } from './pocketbase/Get'; +import { Update } from './pocketbase/Update'; +import { Authentication } from './pocketbase/Authentication'; + +// This script can be imported and executed in the browser console +// to manually test the first-time login functionality + +export async function checkUserSignUpStatus() { + const auth = Authentication.getInstance(); + + if (!auth.isAuthenticated()) { + console.log("User is not authenticated"); + return false; + } + + const user = auth.getCurrentUser(); + if (!user) { + console.log("No current user found"); + return false; + } + + console.log("Current user:", { + id: user.id, + name: user.name || 'Not set', + signed_up: user.signed_up + }); + + return user.signed_up; +} + +export async function resetUserSignUpStatus() { + const auth = Authentication.getInstance(); + + if (!auth.isAuthenticated()) { + console.log("User is not authenticated"); + return false; + } + + const user = auth.getCurrentUser(); + if (!user) { + console.log("No current user found"); + return false; + } + + try { + const update = Update.getInstance(); + await update.updateFields("users", user.id, { + signed_up: false + }); + + console.log("User signed_up status reset to false"); + console.log("Refresh the page to see the onboarding popup"); + + return true; + } catch (error) { + console.error("Error resetting user signed_up status:", error); + return false; + } +} + +// Export for use in browser console +if (typeof window !== 'undefined') { + (window as any).checkUserSignUpStatus = checkUserSignUpStatus; + (window as any).resetUserSignUpStatus = resetUserSignUpStatus; +} \ No newline at end of file diff --git a/src/scripts/pocketbase/Authentication.ts b/src/scripts/pocketbase/Authentication.ts index cba5799..581e960 100644 --- a/src/scripts/pocketbase/Authentication.ts +++ b/src/scripts/pocketbase/Authentication.ts @@ -202,4 +202,18 @@ export class Authentication { console.error("Failed to initialize AuthSyncService:", error); } } + + /** + * Check if the current user has completed the initial sign-up process + * This is used to determine if the onboarding process should be shown + * @returns boolean indicating if the user has signed up or not + */ + public isUserSignedUp(): boolean { + const user = this.getCurrentUser(); + if (!user) return false; + + // If the signed_up field is explicitly set to false, return false + // Otherwise, if it's undefined or true, return true + return user.signed_up !== false; + } }