Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions frontend/src/components/Navigation/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { selectAvatar, selectName } from '@/features/onboardingSelectors';
import { clearSearch } from '@/features/searchSlice';
import { convertFileSrc } from '@tauri-apps/api/core';
import { FaceSearchDialog } from '@/components/Dialog/FaceSearchDialog';
import { Link } from 'react-router';

export function Navbar() {
const userName = useSelector(selectName);
Expand All @@ -20,10 +21,10 @@ export function Navbar() {
<div className="sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b pr-4 backdrop-blur">
{/* Logo */}
<div className="flex w-[256px] items-center justify-center">
<a href="/" className="flex items-center space-x-2">
<Link to="/" className="flex items-center space-x-2">
<img src="/128x128.png" width={32} height={32} alt="PictoPy Logo" />
<span className="text-xl font-bold">PictoPy</span>
</a>
</Link>
</div>

{/* Search Bar */}
Expand Down Expand Up @@ -82,13 +83,13 @@ export function Navbar() {
<span className="hidden text-sm sm:inline-block">
Welcome <span className="text-muted-foreground">{userName}</span>
</span>
<a href="/settings" className="p-2">
<Link to="/settings#account" className="p-2">
<img
src={userAvatar || '/photo1.png'}
className="hover:ring-primary/50 h-8 w-8 cursor-pointer rounded-full transition-all hover:ring-2"
alt="User avatar"
/>
</a>
</Link>
</div>
</div>
</div>
Expand Down
81 changes: 70 additions & 11 deletions frontend/src/pages/SettingsPage/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,87 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router';

// Import modular components
import FolderManagementCard from './components/FolderManagementCard';
import UserPreferencesCard from './components/UserPreferencesCard';
import ApplicationControlsCard from './components/ApplicationControlsCard';
import AccountSettingsCard from './components/AccountSettingsCard';

/**
* Settings page component
* Acts as an orchestrator for the settings sections
*/
const Settings: React.FC = () => {
return (
<div className="mx-auto flex-1 px-8 py-6">
<div className="mx-auto max-w-5xl space-y-8">
{/* Folder Management */}
<FolderManagementCard />
const location = useLocation();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('general');

const handleTabChange = (tab: string) => {
setActiveTab(tab);
navigate(`#${tab}`, { replace: true });
};

{/* User Preferences */}
<UserPreferencesCard />
useEffect(() => {
const hash = location.hash;
if (hash === '#account') {
setActiveTab('account');
} else if (hash === '#general') {
setActiveTab('general');
} else if (hash === '') {
setActiveTab('general');
navigate(`#general`, { replace: true });
}
}, [location.hash]);
const baseTabStyle = 'px-4 py-2 rounded-md font-medium transition-colors';
const activeTabStyle = 'bg-background text-foreground';
const inactiveTabStyle =
'text-muted-foreground hover:bg-gray-100 dark:hover:bg-gray-800';
return (
<>
<h1 className="my-6 text-2xl font-bold">Settings</h1>
<div className="flex-1 pr-3">
<div className="flex flex-col space-y-8">
<div
className="bg-card flex w-fit items-center justify-center gap-1 rounded-lg border p-1"
role="tablist"
>
<button
role="tab"
aria-selected={activeTab === 'general'}
aria-controls="general-panel"
onClick={() => handleTabChange('general')}
className={`${baseTabStyle} ${activeTab === 'general' ? activeTabStyle : inactiveTabStyle}`}
>
General
</button>
<button
role="tab"
aria-selected={activeTab === 'account'}
aria-controls="account-panel"
onClick={() => handleTabChange('account')}
className={`${baseTabStyle} ${activeTab === 'account' ? activeTabStyle : inactiveTabStyle}`}
>
Account
</button>
</div>
<div className="mt-6 space-y-8">
{activeTab === 'general' && (
<>
<FolderManagementCard />
<UserPreferencesCard />
<ApplicationControlsCard />
</>
)}

{/* Application Controls */}
<ApplicationControlsCard />
{activeTab === 'account' && (
<>
<AccountSettingsCard />
</>
)}
</div>
</div>
</div>
</div>
</>
);
};

Expand Down
127 changes: 127 additions & 0 deletions frontend/src/pages/SettingsPage/components/AccountSettingsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useState } from 'react'; // No need for useEffect
import { useDispatch } from 'react-redux';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { setAvatar, setName } from '@/features/onboardingSlice';
import { User } from 'lucide-react';
import SettingsCard from './SettingsCard';
import { avatars } from '@/constants/avatars';
import { CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';

const AccountSettingsCard: React.FC = () => {
const dispatch = useDispatch();
const [name, setLocalName] = useState(
() => localStorage.getItem('name') || '',
);
const [selectedAvatar, setLocalAvatar] = useState(() => {
const stored = localStorage.getItem('avatar') || '';
return avatars.includes(stored) ? stored : '';
});
const [nameError, setNameError] = useState(false);

// The redundant useEffect has been removed.

const handleAvatarSelect = (avatar: string) => {
setLocalAvatar(avatar);
};

const handleNameChange = (value: string) => {
setLocalName(value);
if (nameError) {
setNameError(false);
}
};

const handleSave = () => {
if (!name.trim()) {
setNameError(true);
return;
}

setNameError(false);
if (!selectedAvatar) return;

try {
dispatch(setName(name));
dispatch(setAvatar(selectedAvatar));
localStorage.setItem('name', name);
localStorage.setItem('avatar', selectedAvatar);
} catch (error) {
console.error('Failed to save settings:');
}
};

return (
<SettingsCard
icon={User}
title="Account Information"
description="Manage your account details and profile information."
>
<CardContent className="flex-1 space-y-6 overflow-y-hidden p-1 px-2">
<div className="w-fit space-y-6">
{/* Name Change */}
<div className="w-full">
<Label htmlFor="name" className="mb-2 block text-base font-medium">
Name
</Label>
<Input
id="name"
placeholder={
nameError ? "Name can't be empty" : 'Enter your name'
}
value={name}
onChange={(e) => handleNameChange(e.target.value)}
className={`h-10 w-full text-sm placeholder:text-sm ${
nameError
? 'border-red-500 placeholder:text-red-500/80 focus-visible:ring-red-500'
: ''
}`}
/>
</div>

{/* Avatar Section */}
<div className="w-full">
<Label className="mb-3 block text-base font-medium">Avatar</Label>
<div className="grid grid-cols-4 gap-8">
{avatars.map((avatar) => {
const isSelected = selectedAvatar === avatar;
return (
<button
type="button"
key={avatar}
onClick={() => handleAvatarSelect(avatar)}
className={`bg-background relative inline-flex h-24 w-24 items-center justify-center rounded-full transition-all duration-300 ${
isSelected
? 'ring-offset-background scale-90 ring-2 ring-blue-500 ring-offset-4'
: 'hover:ring-4 hover:ring-blue-500 hover:ring-offset-4'
}`}
>
<img
src={avatar}
alt="Avatar"
className={`h-24 w-24 rounded-full object-cover transition-all duration-300 ${
isSelected ? 'brightness-110' : ''
}`}
/>
</button>
);
})}
</div>
</div>
</div>

{/* Save Changes Button */}
<Button
className="mt-4 w-auto bg-blue-500 px-6 py-2 text-sm font-medium text-white hover:bg-blue-600"
onClick={handleSave}
disabled={!selectedAvatar}
>
Save Changes
</Button>
</CardContent>
</SettingsCard>
);
};

export default AccountSettingsCard;
Loading
Loading