Add file settings

This commit is contained in:
chark1es 2025-03-02 01:14:34 -08:00
parent 60c2ae3f6b
commit b7881213ce
6 changed files with 983 additions and 9 deletions

View file

@ -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>
<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>

View file

@ -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>
);
}

View 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>
);
}

View file

@ -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>
);
}

View 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>
);
}

View file

@ -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">