grocery-app/frontend/src/pages/Settings.jsx
Nico 1281c91c28
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 13s
Build & Deploy Costco Grocery List / deploy (push) Successful in 6s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
add password and display name manipulation
2026-01-24 21:38:33 -08:00

427 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useContext, useEffect, useState } from "react";
import { changePassword, getCurrentUser, updateCurrentUser } from "../api/users";
import { SettingsContext } from "../context/SettingsContext";
import "../styles/pages/Settings.css";
export default function Settings() {
const { settings, updateSettings, resetSettings } = useContext(SettingsContext);
const [activeTab, setActiveTab] = useState("appearance");
// Account management state
const [displayName, setDisplayName] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [accountMessage, setAccountMessage] = useState({ type: "", text: "" });
const [loadingProfile, setLoadingProfile] = useState(false);
const [loadingPassword, setLoadingPassword] = useState(false);
// Load user profile
useEffect(() => {
const loadProfile = async () => {
try {
const response = await getCurrentUser();
setDisplayName(response.data.display_name || response.data.name || "");
} catch (error) {
console.error("Failed to load profile:", error);
}
};
loadProfile();
}, []);
const handleThemeChange = (theme) => {
updateSettings({ theme });
};
const handleUpdateDisplayName = async (e) => {
e.preventDefault();
setLoadingProfile(true);
setAccountMessage({ type: "", text: "" });
try {
await updateCurrentUser(displayName);
setAccountMessage({ type: "success", text: "Display name updated successfully!" });
} catch (error) {
setAccountMessage({
type: "error",
text: error.response?.data?.error || "Failed to update display name"
});
} finally {
setLoadingProfile(false);
}
};
const handleChangePassword = async (e) => {
e.preventDefault();
setLoadingPassword(true);
setAccountMessage({ type: "", text: "" });
if (newPassword !== confirmPassword) {
setAccountMessage({ type: "error", text: "New passwords don't match" });
setLoadingPassword(false);
return;
}
if (newPassword.length < 6) {
setAccountMessage({ type: "error", text: "Password must be at least 6 characters" });
setLoadingPassword(false);
return;
}
try {
await changePassword(currentPassword, newPassword);
setAccountMessage({ type: "success", text: "Password changed successfully!" });
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (error) {
setAccountMessage({
type: "error",
text: error.response?.data?.error || "Failed to change password"
});
} finally {
setLoadingPassword(false);
}
};
const handleToggle = (key) => {
updateSettings({ [key]: !settings[key] });
};
const handleNumberChange = (key, value) => {
updateSettings({ [key]: parseInt(value, 10) });
};
const handleSelectChange = (key, value) => {
updateSettings({ [key]: value });
};
const handleReset = () => {
if (window.confirm("Reset all settings to defaults?")) {
resetSettings();
}
};
return (
<div className="settings-page">
<div className="card" style={{ maxWidth: '800px', margin: '0 auto' }}>
<h1 className="text-2xl font-semibold mb-4">Settings</h1>
<div className="settings-tabs">
<button
className={`settings-tab ${activeTab === "appearance" ? "active" : ""}`}
onClick={() => setActiveTab("appearance")}
>
Appearance
</button>
<button
className={`settings-tab ${activeTab === "list" ? "active" : ""}`}
onClick={() => setActiveTab("list")}
>
List Display
</button>
<button
className={`settings-tab ${activeTab === "behavior" ? "active" : ""}`}
onClick={() => setActiveTab("behavior")}
>
Behavior
</button>
<button
className={`settings-tab ${activeTab === "account" ? "active" : ""}`}
onClick={() => setActiveTab("account")}
>
Account
</button>
</div>
<div className="settings-content">
{/* Appearance Tab */}
{activeTab === "appearance" && (
<div className="settings-section">
<h2 className="text-xl font-semibold mb-4">Appearance</h2>
<div className="settings-group">
<label className="settings-label">Theme</label>
<div className="settings-theme-options">
<button
className={`settings-theme-btn ${settings.theme === "light" ? "active" : ""}`}
onClick={() => handleThemeChange("light")}
>
Light
</button>
<button
className={`settings-theme-btn ${settings.theme === "dark" ? "active" : ""}`}
onClick={() => handleThemeChange("dark")}
>
🌙 Dark
</button>
<button
className={`settings-theme-btn ${settings.theme === "auto" ? "active" : ""}`}
onClick={() => handleThemeChange("auto")}
>
🔄 Auto
</button>
</div>
<p className="settings-description">
Auto mode follows your system preferences
</p>
</div>
<div className="settings-group">
<label className="settings-label">
<input
type="checkbox"
checked={settings.compactView}
onChange={() => handleToggle("compactView")}
/>
<span>Compact View</span>
</label>
<p className="settings-description">
Show more items on screen with reduced spacing
</p>
</div>
</div>
)}
{/* List Display Tab */}
{activeTab === "list" && (
<div className="settings-section">
<h2 className="text-xl font-semibold mb-4">List Display</h2>
<div className="settings-group">
<label className="settings-label">Default Sort Mode</label>
<select
value={settings.defaultSortMode}
onChange={(e) => handleSelectChange("defaultSortMode", e.target.value)}
className="form-select mt-2"
>
<option value="zone">By Zone</option>
<option value="az">A Z</option>
<option value="za">Z A</option>
<option value="qty-high">Quantity: High Low</option>
<option value="qty-low">Quantity: Low High</option>
</select>
<p className="settings-description">
Your preferred sorting method when opening the list
</p>
</div>
<div className="settings-group">
<label className="settings-label">
<input
type="checkbox"
checked={settings.showRecentlyBought}
onChange={() => handleToggle("showRecentlyBought")}
/>
<span>Show Recently Bought Section</span>
</label>
<p className="settings-description">
Display items bought in the last 24 hours
</p>
</div>
{settings.showRecentlyBought && (
<>
<div className="settings-group">
<label className="settings-label">
Recently Bought Item Count: {settings.recentlyBoughtCount}
</label>
<input
type="range"
min="5"
max="50"
step="5"
value={settings.recentlyBoughtCount}
onChange={(e) => handleNumberChange("recentlyBoughtCount", e.target.value)}
className="settings-range"
/>
<p className="settings-description">
Number of items to show initially (5-50)
</p>
</div>
<div className="settings-group">
<label className="settings-label">
<input
type="checkbox"
checked={settings.recentlyBoughtCollapsed}
onChange={() => handleToggle("recentlyBoughtCollapsed")}
/>
<span>Collapse Recently Bought by Default</span>
</label>
<p className="settings-description">
Start with the section collapsed
</p>
</div>
</>
)}
</div>
)}
{/* Behavior Tab */}
{activeTab === "behavior" && (
<div className="settings-section">
<h2 className="text-xl font-semibold mb-4">Behavior</h2>
<div className="settings-group">
<label className="settings-label">
<input
type="checkbox"
checked={settings.confirmBeforeBuy}
onChange={() => handleToggle("confirmBeforeBuy")}
/>
<span>Confirm Before Buying</span>
</label>
<p className="settings-description">
Show confirmation modal when marking items as bought
</p>
</div>
<div className="settings-group">
<label className="settings-label">
Auto-reload Interval (minutes): {settings.autoReloadInterval || "Disabled"}
</label>
<input
type="range"
min="0"
max="30"
step="5"
value={settings.autoReloadInterval}
onChange={(e) => handleNumberChange("autoReloadInterval", e.target.value)}
className="settings-range"
/>
<p className="settings-description">
Automatically refresh the list every X minutes (0 = disabled)
</p>
</div>
<div className="settings-group">
<label className="settings-label">
<input
type="checkbox"
checked={settings.hapticFeedback}
onChange={() => handleToggle("hapticFeedback")}
/>
<span>Haptic Feedback (Mobile)</span>
</label>
<p className="settings-description">
Vibrate on long-press and other interactions
</p>
</div>
</div>
)}
{/* Account Tab */}
{activeTab === "account" && (
<div className="settings-section">
<h2 className="text-xl font-semibold mb-4">Account Management</h2>
{accountMessage.text && (
<div className={`account-message ${accountMessage.type}`}>
{accountMessage.text}
</div>
)}
{/* Display Name Section */}
<form onSubmit={handleUpdateDisplayName} className="account-form">
<h3 className="text-lg font-semibold mb-3">Display Name</h3>
<div className="settings-group">
<label className="settings-label">
Display Name
</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
maxLength={100}
className="form-input"
placeholder="Your display name"
/>
<p className="settings-description">
{displayName.length}/100 characters
</p>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={loadingProfile}
>
{loadingProfile ? "Saving..." : "Save Display Name"}
</button>
</form>
<hr className="my-4" style={{ borderColor: 'var(--border-color)' }} />
{/* Password Change Section */}
<form onSubmit={handleChangePassword} className="account-form">
<h3 className="text-lg font-semibold mb-3">Change Password</h3>
<div className="settings-group">
<label className="settings-label">
Current Password
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="form-input"
required
/>
</div>
<div className="settings-group">
<label className="settings-label">
New Password
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="form-input"
minLength={6}
required
/>
<p className="settings-description">
Minimum 6 characters
</p>
</div>
<div className="settings-group">
<label className="settings-label">
Confirm New Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="form-input"
minLength={6}
required
/>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={loadingPassword}
>
{loadingPassword ? "Changing..." : "Change Password"}
</button>
</form>
</div>
)}
</div>
<div className="mt-4">
<button onClick={handleReset} className="btn btn-outline">
Reset to Defaults
</button>
</div>
</div>
</div>
);
}