grocery-app/frontend/src/pages/Settings.jsx
2026-05-26 00:38:53 -07:00

460 lines
16 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, useRef, useState } from "react";
import { changePassword, getCurrentUser, updateCurrentUser } from "../api/users";
import { SettingsContext } from "../context/SettingsContext";
import useActionToast from "../hooks/useActionToast";
import getApiErrorMessage from "../lib/getApiErrorMessage";
import "../styles/pages/Settings.css";
export default function Settings() {
const { settings, updateSettings, resetSettings } = useContext(SettingsContext);
const toast = useActionToast();
const [activeTab, setActiveTab] = useState("appearance");
const tabsRef = useRef(null);
const [showLeftArrow, setShowLeftArrow] = useState(false);
const [showRightArrow, setShowRightArrow] = useState(false);
// 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();
}, []);
useEffect(() => {
const tabsElement = tabsRef.current;
if (!tabsElement) return;
const updateArrowVisibility = () => {
const hasOverflow = tabsElement.scrollWidth > tabsElement.clientWidth + 1;
if (!hasOverflow) {
setShowLeftArrow(false);
setShowRightArrow(false);
return;
}
setShowLeftArrow(tabsElement.scrollLeft > 4);
setShowRightArrow(
tabsElement.scrollLeft + tabsElement.clientWidth < tabsElement.scrollWidth - 4
);
};
updateArrowVisibility();
tabsElement.addEventListener("scroll", updateArrowVisibility, { passive: true });
window.addEventListener("resize", updateArrowVisibility);
return () => {
tabsElement.removeEventListener("scroll", updateArrowVisibility);
window.removeEventListener("resize", updateArrowVisibility);
};
}, []);
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!" });
toast.success("Updated display name", `Updated display name to ${displayName}`);
} catch (error) {
const message = getApiErrorMessage(error, "Failed to update display name");
setAccountMessage({
type: "error",
text: message
});
toast.error("Update display name failed", `Update display name failed: ${message}`);
} 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!" });
toast.success("Changed password", "Changed password successfully");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (error) {
const message = getApiErrorMessage(error, "Failed to change password");
setAccountMessage({
type: "error",
text: message
});
toast.error("Change password failed", `Change password failed: ${message}`);
} finally {
setLoadingPassword(false);
}
};
const handleToggle = (key) => {
updateSettings({ [key]: !settings[key] });
};
const handleNumberChange = (key, value) => {
updateSettings({ [key]: parseInt(value, 10) });
};
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-wrapper">
<div
className={`settings-tabs-arrow settings-tabs-arrow-left ${showLeftArrow ? "visible" : ""}`}
aria-hidden="true"
>
&#8249;
</div>
<div className="settings-tabs" ref={tabsRef}>
<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-tabs-arrow settings-tabs-arrow-right ${showRightArrow ? "visible" : ""}`}
aria-hidden="true"
>
&#8250;
</div>
</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">
<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>
);
}