From b7881213cea861aa5c52526d0a1856f906ca194c Mon Sep 17 00:00:00 2001 From: chark1es Date: Sun, 2 Mar 2025 01:14:34 -0800 Subject: [PATCH] Add file settings --- .../dashboard/SettingsSection.astro | 84 +++++- .../AccountSecuritySettings.tsx | 169 +++++++++++ .../SettingsSection/DisplaySettings.tsx | 270 ++++++++++++++++++ .../SettingsSection/NotificationSettings.tsx | 238 +++++++++++++++ .../SettingsSection/UserProfileSettings.tsx | 218 ++++++++++++++ src/layouts/Layout.astro | 13 +- 6 files changed, 983 insertions(+), 9 deletions(-) create mode 100644 src/components/dashboard/SettingsSection/AccountSecuritySettings.tsx create mode 100644 src/components/dashboard/SettingsSection/DisplaySettings.tsx create mode 100644 src/components/dashboard/SettingsSection/NotificationSettings.tsx create mode 100644 src/components/dashboard/SettingsSection/UserProfileSettings.tsx 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

+ + +
+
+

+
+ +
+ Profile Information +

+

+ Update your personal information and profile details +

+
+ +
+
+ + +
+
+

+
+ +
+ Account Security +

+

+ Manage your account security settings and authentication options +

+
+ +
+
+ + +
+
+

+
+ +
+ Notification Preferences +

+

+ Customize how and when you receive notifications +

+
+ +
+
+ +
-

Account Settings

-
-

- Account settings will be available soon. -

-
+

+
+ +
+ Display Settings +

+

+ Customize your dashboard appearance and display 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 && ( +
+
+ {successMessage} +
+
+ )} + +
+ {/* Theme Selection */} +
+ + + +
+ + {/* Font Size */} +
+ + + +
+ + {/* Accessibility Options */} +
+ + +
+ + + +
+
+ + {/* Preview */} +
+ +
+
+
+

Theme Preview

+

This is how your content will look with the selected settings.

+
+ + + +
+
+
+
+
+ +
+ +
+
+
+ ); +} \ 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 && ( +
+
+ {successMessage} +
+
+ )} + + {errorMessage && ( +
+
+ {errorMessage} +
+
+ )} + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+ + Note: Some critical notifications about your account cannot be disabled. + +
+
+ +
+ +
+
+
+ ); +} \ 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 && ( +
+
+ {successMessage} +
+
+ )} + + {errorMessage && ( +
+
+ {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"; +