427 lines
15 KiB
JavaScript
427 lines
15 KiB
JavaScript
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>
|
||
);
|
||
}
|