add initial setup for user
This commit is contained in:
parent
48996f4d84
commit
3bda85b7bc
7 changed files with 1780 additions and 735 deletions
|
@ -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"
|
||||
>
|
||||
<option value="">Select a major</option>
|
||||
{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) => (
|
||||
<option key={index} value={major}>
|
||||
{major}
|
||||
</option>
|
||||
))}
|
||||
));
|
||||
})()}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
67
src/components/dashboard/universal/DashboardWrapper.tsx
Normal file
67
src/components/dashboard/universal/DashboardWrapper.tsx
Normal file
|
@ -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 (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<span className="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showOnboarding && (
|
||||
<FirstTimeLoginPopup
|
||||
logtoApiEndpoint={logtoApiEndpoint}
|
||||
onComplete={handleOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardWrapper;
|
56
src/components/dashboard/universal/FirstTimeLoginManager.tsx
Normal file
56
src/components/dashboard/universal/FirstTimeLoginManager.tsx
Normal file
|
@ -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 (
|
||||
<FirstTimeLoginPopup
|
||||
logtoApiEndpoint={logtoApiEndpoint}
|
||||
onComplete={handleOnboardingComplete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FirstTimeLoginManager;
|
701
src/components/dashboard/universal/FirstTimeLoginPopup.tsx
Normal file
701
src/components/dashboard/universal/FirstTimeLoginPopup.tsx
Normal file
|
@ -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<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 overflow-y-auto bg-black bg-opacity-50 flex items-center justify-center p-4"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
variants={overlayVariants}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-base-100 shadow-xl rounded-xl max-w-2xl w-full"
|
||||
variants={popupVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">Complete Your Profile</h2>
|
||||
<div className="badge badge-primary p-3">
|
||||
<Icon icon="heroicons:user" className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="opacity-70 mt-2">
|
||||
Welcome to IEEE UCSD! Please complete your profile to continue.
|
||||
</p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Profile Setup Required"
|
||||
message="You need to complete this information before you can access the dashboard. All fields marked with * are required."
|
||||
className="mt-4"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<motion.span
|
||||
className="loading loading-spinner loading-lg text-primary"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-6">
|
||||
{/* Name Field */}
|
||||
<motion.div
|
||||
className="form-control"
|
||||
custom={0}
|
||||
variants={formItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Full Name <span className="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter your full name"
|
||||
className={`input input-bordered w-full ${!isValid.name && formData.name ? 'input-error' : ''}`}
|
||||
required
|
||||
/>
|
||||
{!isValid.name && formData.name && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">Please enter your full name</span>
|
||||
</label>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Username Field */}
|
||||
<motion.div
|
||||
className="form-control"
|
||||
custom={1}
|
||||
variants={formItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Username <span className="text-error">*</span></span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleInputChange}
|
||||
placeholder="your_username"
|
||||
className={`input input-bordered w-full ${!isValid.username && formData.username ? 'input-error' : ''}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{!isValid.username && formData.username && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">Username must be 3-20 characters, lowercase letters, numbers, and underscores only</span>
|
||||
</label>
|
||||
)}
|
||||
<label className="label">
|
||||
<span className="label-text-alt opacity-70">Choose a unique username for your IEEEUCSD SSO account. This only impacts your SSO login</span>
|
||||
</label>
|
||||
</motion.div>
|
||||
|
||||
{/* PID Field */}
|
||||
<motion.div
|
||||
className="form-control"
|
||||
custom={2}
|
||||
variants={formItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">PID <span className="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="pid"
|
||||
value={formData.pid}
|
||||
onChange={handleInputChange}
|
||||
placeholder="A12345678"
|
||||
className={`input input-bordered w-full ${!isValid.pid && formData.pid ? 'input-error' : ''}`}
|
||||
required
|
||||
/>
|
||||
{!isValid.pid && formData.pid && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">PID must be in format A12345678</span>
|
||||
</label>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Member ID Field (Optional) */}
|
||||
<motion.div
|
||||
className="form-control"
|
||||
custom={3}
|
||||
variants={formItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">IEEE Member ID <span className="text-opacity-50">(optional)</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="member_id"
|
||||
value={formData.member_id}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Your IEEE member ID (if you have one)"
|
||||
className="input input-bordered w-full"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Graduation Year Field */}
|
||||
<motion.div
|
||||
className="form-control"
|
||||
custom={4}
|
||||
variants={formItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Expected Graduation Year <span className="text-error">*</span></span>
|
||||
</label>
|
||||
<select
|
||||
name="graduation_year"
|
||||
value={formData.graduation_year}
|
||||
onChange={handleInputChange}
|
||||
className={`select select-bordered w-full ${!isValid.graduation_year ? 'select-error' : ''}`}
|
||||
required
|
||||
>
|
||||
{Array.from({ length: 10 }, (_, i) => {
|
||||
const year = new Date().getFullYear() + i;
|
||||
return (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
{!isValid.graduation_year && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">Please select a valid graduation year</span>
|
||||
</label>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Major Field */}
|
||||
<motion.div
|
||||
className="form-control"
|
||||
custom={5}
|
||||
variants={formItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Major <span className="text-error">*</span></span>
|
||||
</label>
|
||||
<select
|
||||
name="major"
|
||||
value={formData.major}
|
||||
onChange={handleInputChange}
|
||||
className={`select select-bordered w-full ${!isValid.major && formData.major ? 'select-error' : ''}`}
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Select your major</option>
|
||||
{ucsdMajors.map(major => (
|
||||
<option key={major} value={major}>
|
||||
{major}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{!isValid.major && formData.major && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">Please select your major</span>
|
||||
</label>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Error/Success Messages */}
|
||||
<AnimatePresence>
|
||||
{errorMessage && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<CustomAlert
|
||||
type="error"
|
||||
title="Error Saving Profile"
|
||||
message={errorMessage}
|
||||
icon="heroicons:exclamation-circle"
|
||||
className="mt-4"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{successMessage && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<CustomAlert
|
||||
type="success"
|
||||
title="Profile Saved"
|
||||
message={successMessage}
|
||||
icon="heroicons:check-circle"
|
||||
className="mt-4"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Submit Button */}
|
||||
<motion.div
|
||||
className="form-control mt-8"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6, duration: 0.3 }}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={!canSubmit || isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Complete Profile"
|
||||
)}
|
||||
</button>
|
||||
<p className="text-xs text-center mt-3 opacity-70">
|
||||
<span className="text-error">*</span> Required fields
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default FirstTimeLoginPopup;
|
|
@ -10,9 +10,13 @@ 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;
|
||||
|
@ -28,8 +32,8 @@ const components = Object.fromEntries(
|
|||
);
|
||||
// console.log(`Loaded component: ${section.component}`); // Debug log
|
||||
return [section.component, component.default];
|
||||
}),
|
||||
),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// console.log("Available components:", Object.keys(components)); // Debug log
|
||||
|
@ -47,6 +51,12 @@ const components = Object.fromEntries(
|
|||
></script>
|
||||
</head>
|
||||
<body class="bg-base-200">
|
||||
<!-- First Time Login Manager - This handles the onboarding popup for new users -->
|
||||
<FirstTimeLoginManager
|
||||
client:load
|
||||
logtoApiEndpoint={logtoApiEndpoint}
|
||||
/>
|
||||
|
||||
<div class="flex h-screen">
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
|
@ -65,19 +75,33 @@ const components = Object.fromEntries(
|
|||
<!-- User Profile -->
|
||||
<div class="p-6 border-b border-base-200">
|
||||
<!-- Loading State -->
|
||||
<div id="userProfileSkeleton" class="flex items-center gap-4">
|
||||
<div
|
||||
id="userProfileSkeleton"
|
||||
class="flex items-center gap-4"
|
||||
>
|
||||
<div class="avatar flex items-center justify-center">
|
||||
<div class="w-12 h-12 rounded-xl bg-base-300 animate-pulse"></div>
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-base-300 animate-pulse"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="h-6 w-32 bg-base-300 animate-pulse rounded mb-2">
|
||||
<div
|
||||
class="h-6 w-32 bg-base-300 animate-pulse rounded mb-2"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="h-5 w-20 bg-base-300 animate-pulse rounded"
|
||||
>
|
||||
</div>
|
||||
<div class="h-5 w-20 bg-base-300 animate-pulse rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signed Out State -->
|
||||
<div id="userProfileSignedOut" class="flex items-center gap-4 hidden">
|
||||
<div
|
||||
id="userProfileSignedOut"
|
||||
class="flex items-center gap-4 hidden"
|
||||
>
|
||||
<div class="avatar flex items-center justify-center">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-base-300 text-base-content/30 flex items-center justify-center"
|
||||
|
@ -86,15 +110,22 @@ const components = Object.fromEntries(
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-lg text-base-content/70">
|
||||
<h3
|
||||
class="font-medium text-lg text-base-content/70"
|
||||
>
|
||||
Signed Out
|
||||
</h3>
|
||||
<div class="badge badge-outline mt-1 opacity-50">Guest</div>
|
||||
<div class="badge badge-outline mt-1 opacity-50">
|
||||
Guest
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actual Profile -->
|
||||
<div id="userProfileSummary" class="flex items-center gap-4 hidden">
|
||||
<div
|
||||
id="userProfileSummary"
|
||||
class="flex items-center gap-4 hidden"
|
||||
>
|
||||
<div class="avatar flex items-center justify-center">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-[#06659d] text-white ring ring-base-200 ring-offset-base-100 ring-offset-2 inline-flex items-center justify-center"
|
||||
|
@ -106,7 +137,9 @@ const components = Object.fromEntries(
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-lg" id="userName">Loading...</h3>
|
||||
<h3 class="font-medium text-lg" id="userName">
|
||||
Loading...
|
||||
</h3>
|
||||
<div
|
||||
class="badge badge-outline mt-1 border-[#06659d] text-[#06659d]"
|
||||
id="userRole"
|
||||
|
@ -145,41 +178,64 @@ const components = Object.fromEntries(
|
|||
<div id="actualMenu" class="hidden">
|
||||
{
|
||||
Object.entries(dashboardConfig.categories).map(
|
||||
([categoryKey, category]: [string, any]) => (
|
||||
([categoryKey, category]: [
|
||||
string,
|
||||
any,
|
||||
]) => (
|
||||
<>
|
||||
<li
|
||||
class={`menu-title font-medium opacity-70 ${
|
||||
category.role && category.role !== "none"
|
||||
category.role &&
|
||||
category.role !== "none"
|
||||
? "hidden"
|
||||
: ""
|
||||
}`}
|
||||
data-role-required={category.role || "none"}
|
||||
data-role-required={
|
||||
category.role || "none"
|
||||
}
|
||||
>
|
||||
<span>{category.title}</span>
|
||||
</li>
|
||||
{category.sections.map((sectionKey: string) => {
|
||||
const section = dashboardConfig.sections[sectionKey];
|
||||
{category.sections.map(
|
||||
(sectionKey: string) => {
|
||||
const section =
|
||||
dashboardConfig
|
||||
.sections[
|
||||
sectionKey
|
||||
];
|
||||
return (
|
||||
<li
|
||||
class={
|
||||
section.role && section.role !== "none"
|
||||
section.role &&
|
||||
section.role !==
|
||||
"none"
|
||||
? "hidden"
|
||||
: ""
|
||||
}
|
||||
data-role-required={section.role}
|
||||
data-role-required={
|
||||
section.role
|
||||
}
|
||||
>
|
||||
<button
|
||||
class={`dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5 ${section.class || ""}`}
|
||||
data-section={sectionKey}
|
||||
data-section={
|
||||
sectionKey
|
||||
}
|
||||
>
|
||||
<Icon name={section.icon} class="h-5 w-5" />
|
||||
<Icon
|
||||
name={
|
||||
section.icon
|
||||
}
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
{section.title}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@ -192,10 +248,15 @@ const components = Object.fromEntries(
|
|||
class="flex-1 overflow-x-hidden overflow-y-auto bg-base-200 w-full xl:w-[calc(100%-20rem)]"
|
||||
>
|
||||
<!-- Mobile Header -->
|
||||
<header class="bg-base-100 p-4 shadow-md xl:hidden sticky top-0 z-40">
|
||||
<header
|
||||
class="bg-base-100 p-4 shadow-md xl:hidden sticky top-0 z-40"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="mobileSidebarToggle" class="btn btn-square btn-ghost">
|
||||
<button
|
||||
id="mobileSidebarToggle"
|
||||
class="btn btn-square btn-ghost"
|
||||
>
|
||||
<Icon name="heroicons:bars-3" class="h-6 w-6" />
|
||||
</button>
|
||||
<h1 class="text-xl font-bold">IEEE UCSD</h1>
|
||||
|
@ -207,8 +268,11 @@ const components = Object.fromEntries(
|
|||
<div class="p-4 md:p-6 max-w-[1600px] mx-auto">
|
||||
<!-- Loading State -->
|
||||
<div id="pageLoadingState" class="w-full">
|
||||
<div class="flex flex-col items-center justify-center p-4 sm:p-8">
|
||||
<div class="loading loading-spinner loading-lg"></div>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-4 sm:p-8"
|
||||
>
|
||||
<div class="loading loading-spinner loading-lg">
|
||||
</div>
|
||||
<p class="mt-4 opacity-70">Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -234,7 +298,9 @@ const components = Object.fromEntries(
|
|||
<!-- Not Authenticated State -->
|
||||
<div id="notAuthenticatedState" class="hidden w-full">
|
||||
<div class="card bg-base-100 shadow-xl mx-2 sm:mx-0">
|
||||
<div class="card-body items-center text-center p-4 sm:p-8">
|
||||
<div
|
||||
class="card-body items-center text-center p-4 sm:p-8"
|
||||
>
|
||||
<div class="mb-4 sm:mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -251,9 +317,11 @@ const components = Object.fromEntries(
|
|||
<h2 class="card-title text-xl sm:text-2xl mb-2">
|
||||
Sign in to Access Dashboard
|
||||
</h2>
|
||||
<p class="opacity-70 mb-4 sm:mb-6 text-sm sm:text-base">
|
||||
Please sign in with your IEEE UCSD account to access the
|
||||
dashboard.
|
||||
<p
|
||||
class="opacity-70 mb-4 sm:mb-6 text-sm sm:text-base"
|
||||
>
|
||||
Please sign in with your IEEE UCSD account
|
||||
to access the dashboard.
|
||||
</p>
|
||||
<button
|
||||
class="login-button btn btn-primary btn-lg gap-2 w-full sm:w-auto"
|
||||
|
@ -285,12 +353,14 @@ const components = Object.fromEntries(
|
|||
// Skip if no component is defined
|
||||
if (!section.component) return null;
|
||||
|
||||
const Component = components[section.component];
|
||||
const Component =
|
||||
components[section.component];
|
||||
return (
|
||||
<div
|
||||
id={`${sectionKey}Section`}
|
||||
class={`dashboard-section hidden ${
|
||||
section.role && section.role !== "none"
|
||||
section.role &&
|
||||
section.role !== "none"
|
||||
? "role-restricted"
|
||||
: ""
|
||||
}`}
|
||||
|
@ -299,7 +369,7 @@ const components = Object.fromEntries(
|
|||
<Component />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@ -310,6 +380,24 @@ const components = Object.fromEntries(
|
|||
<!-- Centralized Toast Provider -->
|
||||
<ToastProvider client:load />
|
||||
|
||||
<!-- Only include in development mode -->
|
||||
{
|
||||
import.meta.env.DEV && (
|
||||
<script>
|
||||
import {(checkUserSignUpStatus, resetUserSignUpStatus)} from
|
||||
'../scripts/checkFirstTimeUsers'; // Expose testing
|
||||
utilities to window for development testing
|
||||
window.checkUserSignUpStatus = checkUserSignUpStatus;
|
||||
window.resetUserSignUpStatus = resetUserSignUpStatus;
|
||||
console.log("First-time login test utilities loaded.");
|
||||
console.log("Use window.checkUserSignUpStatus() to check the
|
||||
current signed_up status"); console.log("Use
|
||||
window.resetUserSignUpStatus() to reset the signed_up status
|
||||
to false for testing");
|
||||
</script>
|
||||
)
|
||||
}
|
||||
|
||||
<script>
|
||||
import { Authentication } from "../scripts/pocketbase/Authentication";
|
||||
import { Get } from "../scripts/pocketbase/Get";
|
||||
|
@ -328,10 +416,11 @@ const components = Object.fromEntries(
|
|||
}
|
||||
|
||||
// Initialize page state
|
||||
const pageLoadingState = document.getElementById("pageLoadingState");
|
||||
const pageLoadingState =
|
||||
document.getElementById("pageLoadingState");
|
||||
const pageErrorState = document.getElementById("pageErrorState");
|
||||
const notAuthenticatedState = document.getElementById(
|
||||
"notAuthenticatedState",
|
||||
"notAuthenticatedState"
|
||||
);
|
||||
const mainContent = document.getElementById("mainContent");
|
||||
const sidebar = document.querySelector("aside");
|
||||
|
@ -362,9 +451,11 @@ const components = Object.fromEntries(
|
|||
}
|
||||
|
||||
// For non-sponsor roles, handle normally
|
||||
document.querySelectorAll("[data-role-required]").forEach((element) => {
|
||||
document
|
||||
.querySelectorAll("[data-role-required]")
|
||||
.forEach((element) => {
|
||||
const requiredRole = element.getAttribute(
|
||||
"data-role-required",
|
||||
"data-role-required"
|
||||
) as OfficerStatus;
|
||||
|
||||
// Skip elements that don't have a role requirement
|
||||
|
@ -374,7 +465,10 @@ const components = Object.fromEntries(
|
|||
}
|
||||
|
||||
// Check if user has permission for this role
|
||||
const hasPermission = hasAccess(officerStatus, requiredRole);
|
||||
const hasPermission = hasAccess(
|
||||
officerStatus,
|
||||
requiredRole
|
||||
);
|
||||
|
||||
// Only show elements if user has permission
|
||||
element.classList.toggle("hidden", !hasPermission);
|
||||
|
@ -383,8 +477,10 @@ const components = Object.fromEntries(
|
|||
|
||||
// Handle navigation
|
||||
const handleNavigation = () => {
|
||||
const navButtons = document.querySelectorAll(".dashboard-nav-btn");
|
||||
const sections = document.querySelectorAll(".dashboard-section");
|
||||
const navButtons =
|
||||
document.querySelectorAll(".dashboard-nav-btn");
|
||||
const sections =
|
||||
document.querySelectorAll(".dashboard-section");
|
||||
const mainContentDiv = document.getElementById("mainContent");
|
||||
|
||||
// Ensure mainContent is visible
|
||||
|
@ -418,7 +514,8 @@ const components = Object.fromEntries(
|
|||
|
||||
// Show selected section
|
||||
const sectionId = `${sectionKey}Section`;
|
||||
const targetSection = document.getElementById(sectionId);
|
||||
const targetSection =
|
||||
document.getElementById(sectionId);
|
||||
if (targetSection) {
|
||||
targetSection.classList.remove("hidden");
|
||||
// console.log(`Showing section: ${sectionId}`); // Debug log
|
||||
|
@ -428,7 +525,8 @@ const components = Object.fromEntries(
|
|||
if (window.innerWidth < 1024 && sidebar) {
|
||||
sidebar.classList.add("-translate-x-full");
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
const overlay = document.getElementById("sidebarOverlay");
|
||||
const overlay =
|
||||
document.getElementById("sidebarOverlay");
|
||||
overlay?.remove();
|
||||
}
|
||||
});
|
||||
|
@ -452,6 +550,7 @@ const components = Object.fromEntries(
|
|||
"member_id",
|
||||
"graduation_year",
|
||||
"major",
|
||||
"signed_up",
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -472,7 +571,7 @@ const components = Object.fromEntries(
|
|||
"",
|
||||
{
|
||||
fields: ["id", "type", "role"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (officerRecords && officerRecords.items.length > 0) {
|
||||
|
@ -514,7 +613,7 @@ const components = Object.fromEntries(
|
|||
"",
|
||||
{
|
||||
fields: ["id", "company"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (sponsorRecords && sponsorRecords.items.length > 0) {
|
||||
|
@ -548,7 +647,8 @@ const components = Object.fromEntries(
|
|||
|
||||
if (userName) userName.textContent = fallbackValues.name;
|
||||
if (userRole) userRole.textContent = fallbackValues.role;
|
||||
if (userInitials) userInitials.textContent = fallbackValues.initials;
|
||||
if (userInitials)
|
||||
userInitials.textContent = fallbackValues.initials;
|
||||
|
||||
updateSectionVisibility("" as OfficerStatus);
|
||||
}
|
||||
|
@ -556,16 +656,18 @@ const components = Object.fromEntries(
|
|||
|
||||
// Mobile sidebar toggle
|
||||
const mobileSidebarToggle = document.getElementById(
|
||||
"mobileSidebarToggle",
|
||||
"mobileSidebarToggle"
|
||||
);
|
||||
if (mobileSidebarToggle && sidebar) {
|
||||
const toggleSidebar = () => {
|
||||
const isOpen = !sidebar.classList.contains("-translate-x-full");
|
||||
const isOpen =
|
||||
!sidebar.classList.contains("-translate-x-full");
|
||||
|
||||
if (isOpen) {
|
||||
sidebar.classList.add("-translate-x-full");
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
const overlay = document.getElementById("sidebarOverlay");
|
||||
const overlay =
|
||||
document.getElementById("sidebarOverlay");
|
||||
overlay?.remove();
|
||||
} else {
|
||||
sidebar.classList.remove("-translate-x-full");
|
||||
|
@ -600,7 +702,8 @@ const components = Object.fromEntries(
|
|||
window.toast = originalToast;
|
||||
|
||||
// console.log("User not authenticated");
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.remove("hidden");
|
||||
return;
|
||||
|
@ -609,28 +712,30 @@ const components = Object.fromEntries(
|
|||
// Initialize auth sync for IndexedDB (for authenticated users)
|
||||
await initAuthSync();
|
||||
|
||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.remove("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.add("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.add("hidden");
|
||||
|
||||
// Show loading states
|
||||
const userProfileSkeleton = document.getElementById(
|
||||
"userProfileSkeleton",
|
||||
"userProfileSkeleton"
|
||||
);
|
||||
const userProfileSignedOut = document.getElementById(
|
||||
"userProfileSignedOut",
|
||||
"userProfileSignedOut"
|
||||
);
|
||||
const userProfileSummary =
|
||||
document.getElementById("userProfileSummary");
|
||||
const menuLoadingSkeleton = document.getElementById(
|
||||
"menuLoadingSkeleton",
|
||||
"menuLoadingSkeleton"
|
||||
);
|
||||
const actualMenu = document.getElementById("actualMenu");
|
||||
|
||||
if (userProfileSkeleton)
|
||||
userProfileSkeleton.classList.remove("hidden");
|
||||
if (userProfileSummary) userProfileSummary.classList.add("hidden");
|
||||
if (userProfileSummary)
|
||||
userProfileSummary.classList.add("hidden");
|
||||
if (userProfileSignedOut)
|
||||
userProfileSignedOut.classList.add("hidden");
|
||||
if (menuLoadingSkeleton)
|
||||
|
@ -641,11 +746,15 @@ const components = Object.fromEntries(
|
|||
await updateUserProfile(user);
|
||||
|
||||
// Show actual profile and hide skeleton
|
||||
if (userProfileSkeleton) userProfileSkeleton.classList.add("hidden");
|
||||
if (userProfileSummary) userProfileSummary.classList.remove("hidden");
|
||||
if (userProfileSkeleton)
|
||||
userProfileSkeleton.classList.add("hidden");
|
||||
if (userProfileSummary)
|
||||
userProfileSummary.classList.remove("hidden");
|
||||
|
||||
// Hide all sections first
|
||||
document.querySelectorAll(".dashboard-section").forEach((section) => {
|
||||
document
|
||||
.querySelectorAll(".dashboard-section")
|
||||
.forEach((section) => {
|
||||
section.classList.add("hidden");
|
||||
});
|
||||
|
||||
|
@ -662,7 +771,7 @@ const components = Object.fromEntries(
|
|||
"",
|
||||
{
|
||||
fields: ["id", "type", "role"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (officerRecords && officerRecords.items.length > 0) {
|
||||
|
@ -700,17 +809,23 @@ const components = Object.fromEntries(
|
|||
"",
|
||||
{
|
||||
fields: ["id", "company"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (sponsorRecords && sponsorRecords.items.length > 0) {
|
||||
if (
|
||||
sponsorRecords &&
|
||||
sponsorRecords.items.length > 0
|
||||
) {
|
||||
officerStatus = "sponsor";
|
||||
} else {
|
||||
officerStatus = "none";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error determining officer status:", error);
|
||||
console.error(
|
||||
"Error determining officer status:",
|
||||
error
|
||||
);
|
||||
officerStatus = "none";
|
||||
}
|
||||
|
||||
|
@ -721,14 +836,19 @@ const components = Object.fromEntries(
|
|||
// Only sponsors get a different default view
|
||||
if (officerStatus === "sponsor") {
|
||||
// For sponsors, show the sponsor dashboard
|
||||
defaultSection = document.getElementById("sponsorDashboardSection");
|
||||
defaultSection = document.getElementById(
|
||||
"sponsorDashboardSection"
|
||||
);
|
||||
defaultButton = document.querySelector(
|
||||
'[data-section="sponsorDashboard"]',
|
||||
'[data-section="sponsorDashboard"]'
|
||||
);
|
||||
} else {
|
||||
// For all other users (including administrators), show the profile section
|
||||
defaultSection = document.getElementById("profileSection");
|
||||
defaultButton = document.querySelector('[data-section="profile"]');
|
||||
defaultSection =
|
||||
document.getElementById("profileSection");
|
||||
defaultButton = document.querySelector(
|
||||
'[data-section="profile"]'
|
||||
);
|
||||
|
||||
// Log the default section for debugging
|
||||
// console.log(`Setting default section to profile for user with role: ${officerStatus}`);
|
||||
|
@ -745,16 +865,20 @@ const components = Object.fromEntries(
|
|||
handleNavigation();
|
||||
|
||||
// Show actual menu and hide skeleton
|
||||
if (menuLoadingSkeleton) menuLoadingSkeleton.classList.add("hidden");
|
||||
if (menuLoadingSkeleton)
|
||||
menuLoadingSkeleton.classList.add("hidden");
|
||||
if (actualMenu) actualMenu.classList.remove("hidden");
|
||||
|
||||
// Show main content and hide loading
|
||||
if (mainContent) mainContent.classList.remove("hidden");
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
} catch (error) {
|
||||
console.error("Error initializing dashboard:", error);
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState)
|
||||
pageErrorState.classList.remove("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -766,19 +890,24 @@ const components = Object.fromEntries(
|
|||
.querySelector(".login-button")
|
||||
?.addEventListener("click", async () => {
|
||||
try {
|
||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.remove("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.add("hidden");
|
||||
await auth.login();
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState)
|
||||
pageErrorState.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle logout button click
|
||||
document.getElementById("logoutButton")?.addEventListener("click", () => {
|
||||
document
|
||||
.getElementById("logoutButton")
|
||||
?.addEventListener("click", () => {
|
||||
auth.logout();
|
||||
window.location.reload();
|
||||
});
|
||||
|
@ -791,7 +920,8 @@ const components = Object.fromEntries(
|
|||
|
||||
window.addEventListener("resize", () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
const overlay = document.getElementById("sidebarOverlay");
|
||||
const overlay =
|
||||
document.getElementById("sidebarOverlay");
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
|
|
70
src/scripts/checkFirstTimeUsers.ts
Normal file
70
src/scripts/checkFirstTimeUsers.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue