diff --git a/src/components/dashboard/SettingsSection.astro b/src/components/dashboard/SettingsSection.astro
index 4949499..8723219 100644
--- a/src/components/dashboard/SettingsSection.astro
+++ b/src/components/dashboard/SettingsSection.astro
@@ -1,22 +1,90 @@
---
import { Icon } from "astro-icon/components";
+import UserProfileSettings from "./SettingsSection/UserProfileSettings";
+import AccountSecuritySettings from "./SettingsSection/AccountSecuritySettings";
+import NotificationSettings from "./SettingsSection/NotificationSettings";
+import DisplaySettings from "./SettingsSection/DisplaySettings";
---
-
+
Settings
-
Manage your account settings
+
Manage your account settings and preferences
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/dashboard/SettingsSection/AccountSecuritySettings.tsx b/src/components/dashboard/SettingsSection/AccountSecuritySettings.tsx
new file mode 100644
index 0000000..e5ab5fd
--- /dev/null
+++ b/src/components/dashboard/SettingsSection/AccountSecuritySettings.tsx
@@ -0,0 +1,169 @@
+import { useState, useEffect } from 'react';
+import { Authentication } from '../../../scripts/pocketbase/Authentication';
+import { SendLog } from '../../../scripts/pocketbase/SendLog';
+
+export default function AccountSecuritySettings() {
+ const auth = Authentication.getInstance();
+ const logger = SendLog.getInstance();
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [sessionInfo, setSessionInfo] = useState({
+ lastLogin: '',
+ browser: '',
+ device: '',
+ });
+
+ useEffect(() => {
+ const checkAuth = () => {
+ const authenticated = auth.isAuthenticated();
+ setIsAuthenticated(authenticated);
+
+ if (authenticated) {
+ const user = auth.getCurrentUser();
+ if (user) {
+ // Get last login time
+ const lastLogin = user.last_login || user.updated;
+
+ // Get browser and device info
+ const userAgent = navigator.userAgent;
+ const browser = detectBrowser(userAgent);
+ const device = detectDevice(userAgent);
+
+ setSessionInfo({
+ lastLogin: formatDate(lastLogin),
+ browser,
+ device,
+ });
+ }
+ }
+
+ setLoading(false);
+ };
+
+ checkAuth();
+ }, []);
+
+ const handleLogout = async () => {
+ try {
+ await logger.send('logout', 'auth', 'User manually logged out from settings page');
+ await auth.logout();
+ window.location.href = '/';
+ } catch (error) {
+ console.error('Error during logout:', error);
+ }
+ };
+
+ const detectBrowser = (userAgent: string): string => {
+ if (userAgent.indexOf('Chrome') > -1) return 'Chrome';
+ if (userAgent.indexOf('Safari') > -1) return 'Safari';
+ if (userAgent.indexOf('Firefox') > -1) return 'Firefox';
+ if (userAgent.indexOf('MSIE') > -1 || userAgent.indexOf('Trident') > -1) return 'Internet Explorer';
+ if (userAgent.indexOf('Edge') > -1) return 'Edge';
+ return 'Unknown Browser';
+ };
+
+ const detectDevice = (userAgent: string): string => {
+ if (/Android/i.test(userAgent)) return 'Android Device';
+ if (/iPhone|iPad|iPod/i.test(userAgent)) return 'iOS Device';
+ if (/Windows/i.test(userAgent)) return 'Windows Device';
+ if (/Mac/i.test(userAgent)) return 'Mac Device';
+ if (/Linux/i.test(userAgent)) return 'Linux Device';
+ return 'Unknown Device';
+ };
+
+ const formatDate = (dateString: string): string => {
+ if (!dateString) return 'Unknown';
+
+ const date = new Date(dateString);
+ return new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ timeZoneName: 'short'
+ }).format(date);
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+
+ You must be logged in to access this page.
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Current Session Information */}
+
+
Current Session
+
+
+
Last Login
+
{sessionInfo.lastLogin}
+
+
+
Browser
+
{sessionInfo.browser}
+
+
+
Device
+
{sessionInfo.device}
+
+
+
+
+ {/* Authentication Options */}
+
+
Authentication Options
+
+ IEEE UCSD uses Single Sign-On (SSO) through UCSD for authentication.
+ Password management is handled through your UCSD account.
+
+
+
+
+ To change your password, please visit the UCSD SSO portal.
+
+
+
+
+ {/* Account Actions */}
+
+
Account Actions
+
+
+
+
+
+
+
+ If you need to delete your account or have other account-related issues,
+ please contact an IEEE UCSD administrator.
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/dashboard/SettingsSection/DisplaySettings.tsx b/src/components/dashboard/SettingsSection/DisplaySettings.tsx
new file mode 100644
index 0000000..ce359ca
--- /dev/null
+++ b/src/components/dashboard/SettingsSection/DisplaySettings.tsx
@@ -0,0 +1,270 @@
+import { useState, useEffect } from 'react';
+
+export default function DisplaySettings() {
+ const [theme, setTheme] = useState('dark');
+ const [fontSize, setFontSize] = useState('medium');
+ const [colorBlindMode, setColorBlindMode] = useState(false);
+ const [reducedMotion, setReducedMotion] = useState(false);
+ const [successMessage, setSuccessMessage] = useState('');
+
+ // Load saved preferences from localStorage on component mount
+ useEffect(() => {
+ const savedTheme = localStorage.getItem('theme') || 'dark';
+ const savedFontSize = localStorage.getItem('fontSize') || 'medium';
+ const savedColorBlindMode = localStorage.getItem('colorBlindMode') === 'true';
+ const savedReducedMotion = localStorage.getItem('reducedMotion') === 'true';
+
+ setTheme(savedTheme);
+ setFontSize(savedFontSize);
+ setColorBlindMode(savedColorBlindMode);
+ setReducedMotion(savedReducedMotion);
+
+ // Apply theme to document
+ document.documentElement.setAttribute('data-theme', savedTheme);
+
+ // Apply font size
+ applyFontSize(savedFontSize);
+
+ // Apply accessibility settings
+ if (savedColorBlindMode) {
+ document.documentElement.classList.add('color-blind-mode');
+ }
+
+ if (savedReducedMotion) {
+ document.documentElement.classList.add('reduced-motion');
+ }
+ }, []);
+
+ // Apply font size to document
+ const applyFontSize = (size: string) => {
+ const htmlElement = document.documentElement;
+
+ // Remove existing font size classes
+ htmlElement.classList.remove('text-sm', 'text-base', 'text-lg', 'text-xl');
+
+ // Add new font size class
+ switch (size) {
+ case 'small':
+ htmlElement.classList.add('text-sm');
+ break;
+ case 'medium':
+ htmlElement.classList.add('text-base');
+ break;
+ case 'large':
+ htmlElement.classList.add('text-lg');
+ break;
+ case 'extra-large':
+ htmlElement.classList.add('text-xl');
+ break;
+ }
+ };
+
+ // Handle theme change
+ const handleThemeChange = (e: React.ChangeEvent
) => {
+ const newTheme = e.target.value;
+ setTheme(newTheme);
+
+ // Apply theme to document
+ document.documentElement.setAttribute('data-theme', newTheme);
+
+ // Save to localStorage
+ localStorage.setItem('theme', newTheme);
+ };
+
+ // Handle font size change
+ const handleFontSizeChange = (e: React.ChangeEvent) => {
+ const newSize = e.target.value;
+ setFontSize(newSize);
+
+ // Apply font size
+ applyFontSize(newSize);
+
+ // Save to localStorage
+ localStorage.setItem('fontSize', newSize);
+ };
+
+ // Handle color blind mode toggle
+ const handleColorBlindModeChange = (e: React.ChangeEvent) => {
+ const enabled = e.target.checked;
+ setColorBlindMode(enabled);
+
+ // Apply to document
+ if (enabled) {
+ document.documentElement.classList.add('color-blind-mode');
+ } else {
+ document.documentElement.classList.remove('color-blind-mode');
+ }
+
+ // Save to localStorage
+ localStorage.setItem('colorBlindMode', enabled.toString());
+ };
+
+ // Handle reduced motion toggle
+ const handleReducedMotionChange = (e: React.ChangeEvent) => {
+ const enabled = e.target.checked;
+ setReducedMotion(enabled);
+
+ // Apply to document
+ if (enabled) {
+ document.documentElement.classList.add('reduced-motion');
+ } else {
+ document.documentElement.classList.remove('reduced-motion');
+ }
+
+ // Save to localStorage
+ localStorage.setItem('reducedMotion', enabled.toString());
+ };
+
+ // Handle form submission
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Show success message
+ setSuccessMessage('Display settings saved successfully!');
+
+ // Clear success message after 3 seconds
+ setTimeout(() => {
+ setSuccessMessage('');
+ }, 3000);
+ };
+
+ return (
+
+ {successMessage && (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/dashboard/SettingsSection/NotificationSettings.tsx b/src/components/dashboard/SettingsSection/NotificationSettings.tsx
new file mode 100644
index 0000000..12cf895
--- /dev/null
+++ b/src/components/dashboard/SettingsSection/NotificationSettings.tsx
@@ -0,0 +1,238 @@
+import { useState, useEffect } from 'react';
+import { Authentication } from '../../../scripts/pocketbase/Authentication';
+import { Update } from '../../../scripts/pocketbase/Update';
+import { Collections } from '../../../schemas/pocketbase/schema';
+
+export default function NotificationSettings() {
+ const auth = Authentication.getInstance();
+ const update = Update.getInstance();
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [successMessage, setSuccessMessage] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+
+ // Notification preferences
+ const [preferences, setPreferences] = useState({
+ emailNotifications: true,
+ eventReminders: true,
+ eventUpdates: true,
+ reimbursementUpdates: true,
+ officerAnnouncements: true,
+ marketingEmails: false
+ });
+
+ useEffect(() => {
+ const loadPreferences = async () => {
+ try {
+ const user = auth.getCurrentUser();
+ if (user) {
+ // If user has notification_preferences, parse and use them
+ // Otherwise use defaults
+ if (user.notification_preferences) {
+ try {
+ const savedPrefs = JSON.parse(user.notification_preferences);
+ setPreferences(prev => ({
+ ...prev,
+ ...savedPrefs
+ }));
+ } catch (e) {
+ console.error('Error parsing notification preferences:', e);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error loading notification preferences:', error);
+ setErrorMessage('Failed to load notification preferences');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadPreferences();
+ }, []);
+
+ const handleToggleChange = (e: React.ChangeEvent) => {
+ const { name, checked } = e.target;
+ setPreferences(prev => ({
+ ...prev,
+ [name]: checked
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSaving(true);
+ setSuccessMessage('');
+ setErrorMessage('');
+
+ try {
+ const user = auth.getCurrentUser();
+ if (!user) throw new Error('User not authenticated');
+
+ // Save preferences as JSON string
+ await update.updateFields(
+ Collections.USERS,
+ user.id,
+ { notification_preferences: JSON.stringify(preferences) }
+ );
+
+ setSuccessMessage('Notification preferences saved successfully!');
+
+ // Clear success message after 3 seconds
+ setTimeout(() => {
+ setSuccessMessage('');
+ }, 3000);
+ } catch (error) {
+ console.error('Error saving notification preferences:', error);
+ setErrorMessage('Failed to save notification preferences');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {successMessage && (
+
+ )}
+
+ {errorMessage && (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/dashboard/SettingsSection/UserProfileSettings.tsx b/src/components/dashboard/SettingsSection/UserProfileSettings.tsx
new file mode 100644
index 0000000..c14a9fa
--- /dev/null
+++ b/src/components/dashboard/SettingsSection/UserProfileSettings.tsx
@@ -0,0 +1,218 @@
+import { useState, useEffect } from 'react';
+import { Authentication } from '../../../scripts/pocketbase/Authentication';
+import { Update } from '../../../scripts/pocketbase/Update';
+import { Collections, type User } from '../../../schemas/pocketbase/schema';
+
+export default function UserProfileSettings() {
+ const auth = Authentication.getInstance();
+ const update = Update.getInstance();
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ major: '',
+ graduation_year: '',
+ zelle_information: ''
+ });
+ const [successMessage, setSuccessMessage] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+
+ useEffect(() => {
+ const loadUserData = async () => {
+ try {
+ const currentUser = auth.getCurrentUser();
+ if (currentUser) {
+ setUser(currentUser);
+ setFormData({
+ name: currentUser.name || '',
+ email: currentUser.email || '',
+ major: currentUser.major || '',
+ graduation_year: currentUser.graduation_year?.toString() || '',
+ zelle_information: currentUser.zelle_information || ''
+ });
+ }
+ } catch (error) {
+ console.error('Error loading user data:', error);
+ setErrorMessage('Failed to load user data. Please try again later.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadUserData();
+ }, []);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSaving(true);
+ setSuccessMessage('');
+ setErrorMessage('');
+
+ try {
+ if (!user) throw new Error('User not authenticated');
+
+ const updateData: Partial = {
+ name: formData.name,
+ major: formData.major || undefined,
+ zelle_information: formData.zelle_information || undefined
+ };
+
+ // Only include graduation_year if it's a valid number
+ if (formData.graduation_year && !isNaN(Number(formData.graduation_year))) {
+ updateData.graduation_year = Number(formData.graduation_year);
+ }
+
+ await update.updateFields(Collections.USERS, user.id, updateData);
+
+ // Update local user state
+ setUser(prev => prev ? { ...prev, ...updateData } : null);
+
+ setSuccessMessage('Profile updated successfully!');
+
+ // Clear success message after 3 seconds
+ setTimeout(() => {
+ setSuccessMessage('');
+ }, 3000);
+ } catch (error) {
+ console.error('Error updating profile:', error);
+ setErrorMessage('Failed to update profile. Please try again.');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+
+ You must be logged in to access this page.
+
+
+ );
+ }
+
+ return (
+
+ {successMessage && (
+
+ )}
+
+ {errorMessage && (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 0418365..e9b173b 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -5,7 +5,7 @@ import InView from "../components/core/InView.astro";
---
-
+
@@ -15,6 +15,17 @@ import InView from "../components/core/InView.astro";
+