Add file settings
This commit is contained in:
parent
60c2ae3f6b
commit
b7881213ce
6 changed files with 983 additions and 9 deletions
|
@ -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";
|
||||
---
|
||||
|
||||
<div id="" class="">
|
||||
<div id="settings-section" class="">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold">Settings</h2>
|
||||
<p class="opacity-70">Manage your account settings</p>
|
||||
<p class="opacity-70">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<!-- Profile Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:user" class="h-5 w-5" />
|
||||
</div>
|
||||
Profile Information
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Update your personal information and profile details
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<UserProfileSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Security Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:lock-closed" class="h-5 w-5" />
|
||||
</div>
|
||||
Account Security
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Manage your account security settings and authentication options
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<AccountSecuritySettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:bell" class="h-5 w-5" />
|
||||
</div>
|
||||
Notification Preferences
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize how and when you receive notifications
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<NotificationSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Account Settings</h3>
|
||||
<div class="py-4">
|
||||
<p class="text-base-content/70">
|
||||
Account settings will be available soon.
|
||||
</p>
|
||||
</div>
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
|
||||
</div>
|
||||
Display Settings
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize your dashboard appearance and display preferences
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<DisplaySettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="loading loading-spinner loading-lg"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
<div>
|
||||
<span>You must be logged in to access this page.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-6">
|
||||
{/* Current Session Information */}
|
||||
<div className="bg-base-200 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-lg mb-2">Current Session</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm opacity-70">Last Login</p>
|
||||
<p className="font-medium">{sessionInfo.lastLogin}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm opacity-70">Browser</p>
|
||||
<p className="font-medium">{sessionInfo.browser}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm opacity-70">Device</p>
|
||||
<p className="font-medium">{sessionInfo.device}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authentication Options */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg mb-2">Authentication Options</h4>
|
||||
<p className="text-sm opacity-70 mb-4">
|
||||
IEEE UCSD uses Single Sign-On (SSO) through UCSD for authentication.
|
||||
Password management is handled through your UCSD account.
|
||||
</p>
|
||||
|
||||
<div className="alert alert-info">
|
||||
<div>
|
||||
<span>To change your password, please visit the UCSD SSO portal.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Actions */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg mb-2">Account Actions</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="btn btn-error btn-outline w-full md:w-auto"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
|
||||
<div className="alert alert-warning">
|
||||
<div>
|
||||
<span>
|
||||
If you need to delete your account or have other account-related issues,
|
||||
please contact an IEEE UCSD administrator.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
270
src/components/dashboard/SettingsSection/DisplaySettings.tsx
Normal file
270
src/components/dashboard/SettingsSection/DisplaySettings.tsx
Normal file
|
@ -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<HTMLSelectElement>) => {
|
||||
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<HTMLSelectElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div>
|
||||
{successMessage && (
|
||||
<div className="alert alert-success mb-4">
|
||||
<div>
|
||||
<span>{successMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Theme Selection */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Theme</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={theme}
|
||||
onChange={handleThemeChange}
|
||||
>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="cupcake">Cupcake</option>
|
||||
<option value="bumblebee">Bumblebee</option>
|
||||
<option value="emerald">Emerald</option>
|
||||
<option value="corporate">Corporate</option>
|
||||
<option value="synthwave">Synthwave</option>
|
||||
<option value="retro">Retro</option>
|
||||
<option value="cyberpunk">Cyberpunk</option>
|
||||
<option value="valentine">Valentine</option>
|
||||
<option value="halloween">Halloween</option>
|
||||
<option value="garden">Garden</option>
|
||||
<option value="forest">Forest</option>
|
||||
<option value="aqua">Aqua</option>
|
||||
<option value="lofi">Lo-Fi</option>
|
||||
<option value="pastel">Pastel</option>
|
||||
<option value="fantasy">Fantasy</option>
|
||||
<option value="wireframe">Wireframe</option>
|
||||
<option value="black">Black</option>
|
||||
<option value="luxury">Luxury</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
<option value="cmyk">CMYK</option>
|
||||
<option value="autumn">Autumn</option>
|
||||
<option value="business">Business</option>
|
||||
<option value="acid">Acid</option>
|
||||
<option value="lemonade">Lemonade</option>
|
||||
<option value="night">Night</option>
|
||||
<option value="coffee">Coffee</option>
|
||||
<option value="winter">Winter</option>
|
||||
</select>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Choose a theme for your dashboard</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Font Size */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Font Size</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={fontSize}
|
||||
onChange={handleFontSizeChange}
|
||||
>
|
||||
<option value="small">Small</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large">Large</option>
|
||||
<option value="extra-large">Extra Large</option>
|
||||
</select>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Adjust the text size for better readability</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Accessibility Options */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Accessibility Options</span>
|
||||
</label>
|
||||
|
||||
<div className="space-y-4 p-4 bg-base-200 rounded-lg">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={colorBlindMode}
|
||||
onChange={handleColorBlindModeChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Color Blind Mode</span>
|
||||
<p className="text-xs opacity-70">Enhances color contrast for better visibility</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={reducedMotion}
|
||||
onChange={handleReducedMotionChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Reduced Motion</span>
|
||||
<p className="text-xs opacity-70">Minimizes animations and transitions</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Preview</span>
|
||||
</label>
|
||||
<div className="p-4 bg-base-200 rounded-lg">
|
||||
<div className="card bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">Theme Preview</h3>
|
||||
<p>This is how your content will look with the selected settings.</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button className="btn btn-primary">Primary</button>
|
||||
<button className="btn btn-secondary">Secondary</button>
|
||||
<button className="btn btn-accent">Accent</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control mt-6">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="loading loading-spinner loading-lg"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{successMessage && (
|
||||
<div className="alert alert-success mb-4">
|
||||
<div>
|
||||
<span>{successMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<div>
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="emailNotifications"
|
||||
className="toggle toggle-primary"
|
||||
checked={preferences.emailNotifications}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Email Notifications</span>
|
||||
<p className="text-xs opacity-70">Receive notifications via email</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="eventReminders"
|
||||
className="toggle toggle-primary"
|
||||
checked={preferences.eventReminders}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Event Reminders</span>
|
||||
<p className="text-xs opacity-70">Receive reminders about upcoming events</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="eventUpdates"
|
||||
className="toggle toggle-primary"
|
||||
checked={preferences.eventUpdates}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Event Updates</span>
|
||||
<p className="text-xs opacity-70">Receive updates about events you've registered for</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="reimbursementUpdates"
|
||||
className="toggle toggle-primary"
|
||||
checked={preferences.reimbursementUpdates}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Reimbursement Updates</span>
|
||||
<p className="text-xs opacity-70">Receive updates about your reimbursement requests</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="officerAnnouncements"
|
||||
className="toggle toggle-primary"
|
||||
checked={preferences.officerAnnouncements}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Officer Announcements</span>
|
||||
<p className="text-xs opacity-70">Receive important announcements from IEEE UCSD officers</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="marketingEmails"
|
||||
className="toggle toggle-primary"
|
||||
checked={preferences.marketingEmails}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Marketing Emails</span>
|
||||
<p className="text-xs opacity-70">Receive promotional emails about IEEE UCSD events and opportunities</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="alert alert-info mt-6 mb-6">
|
||||
<div>
|
||||
<span>
|
||||
Note: Some critical notifications about your account cannot be disabled.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Preferences'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
218
src/components/dashboard/SettingsSection/UserProfileSettings.tsx
Normal file
218
src/components/dashboard/SettingsSection/UserProfileSettings.tsx
Normal file
|
@ -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<User | null>(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<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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<User> = {
|
||||
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 (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="loading loading-spinner loading-lg"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
<div>
|
||||
<span>You must be logged in to access this page.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{successMessage && (
|
||||
<div className="alert alert-success mb-4">
|
||||
<div>
|
||||
<span>{successMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<div>
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Full Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
className="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Email Address</span>
|
||||
<span className="label-text-alt text-info">Cannot be changed</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
className="input input-bordered w-full"
|
||||
disabled
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Email changes must be processed by an administrator</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Major</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="major"
|
||||
value={formData.major}
|
||||
onChange={handleInputChange}
|
||||
className="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Graduation Year</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="graduation_year"
|
||||
value={formData.graduation_year}
|
||||
onChange={handleInputChange}
|
||||
className="input input-bordered w-full"
|
||||
min="2000"
|
||||
max="2100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Zelle Information (for reimbursements)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="zelle_information"
|
||||
value={formData.zelle_information}
|
||||
onChange={handleInputChange}
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Email or phone number associated with your Zelle account"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,7 @@ import InView from "../components/core/InView.astro";
|
|||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="w-full h-full m-0 bg-ieee-black">
|
||||
<html lang="en" data-theme="dark" class="w-full h-full m-0 bg-ieee-black">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
@ -15,6 +15,17 @@ import InView from "../components/core/InView.astro";
|
|||
<script
|
||||
src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"
|
||||
></script>
|
||||
<script is:inline>
|
||||
// Set default theme to dark if not already set
|
||||
if (!localStorage.getItem("theme")) {
|
||||
localStorage.setItem("theme", "dark");
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
// Apply saved theme
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
document.documentElement.setAttribute("data-theme", savedTheme);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<InView />
|
||||
<body class="w-full h-full m-0 bg-ieee-black">
|
||||
|
|
Loading…
Reference in a new issue