Included household/stores management features
This commit is contained in:
parent
9fc25f2274
commit
213134c4a5
241
HOUSEHOLD_MANAGEMENT_IMPLEMENTATION.md
Normal file
241
HOUSEHOLD_MANAGEMENT_IMPLEMENTATION.md
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
# Household & Store Management - Implementation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Built comprehensive household and store management UI for the multi-household grocery list application. Users can now fully manage their households, members, and stores through a polished interface.
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
### 1. Manage Page (`/manage`)
|
||||||
|
**Location**: [frontend/src/pages/Manage.jsx](frontend/src/pages/Manage.jsx)
|
||||||
|
|
||||||
|
- Tab-based interface for Household and Store management
|
||||||
|
- Context-aware - always operates on the active household
|
||||||
|
- Accessible via "Manage" link in the navbar
|
||||||
|
|
||||||
|
### 2. Household Management
|
||||||
|
**Component**: [frontend/src/components/manage/ManageHousehold.jsx](frontend/src/components/manage/ManageHousehold.jsx)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- **Edit Household Name**: Admin-only, inline editing
|
||||||
|
- **Invite Code Management**:
|
||||||
|
- Show/hide invite code with copy-to-clipboard
|
||||||
|
- Generate new invite code (invalidates old one)
|
||||||
|
- Admin-only access
|
||||||
|
- **Member Management**:
|
||||||
|
- View all household members with roles
|
||||||
|
- Promote/demote members between admin and member roles
|
||||||
|
- Remove members from household
|
||||||
|
- Cannot remove yourself
|
||||||
|
- Admin-only actions
|
||||||
|
- **Delete Household**:
|
||||||
|
- Admin-only
|
||||||
|
- Double confirmation required
|
||||||
|
- Permanently deletes all data
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
- Viewers: Can only see household name and members
|
||||||
|
- Members: Same as viewers
|
||||||
|
- Admins: Full access to all features
|
||||||
|
|
||||||
|
### 3. Store Management
|
||||||
|
**Component**: [frontend/src/components/manage/ManageStores.jsx](frontend/src/components/manage/ManageStores.jsx)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- **View Household Stores**:
|
||||||
|
- Grid layout showing all stores
|
||||||
|
- Shows store name, location, and default status
|
||||||
|
- **Add Stores**:
|
||||||
|
- Select from system-wide store catalog
|
||||||
|
- Admin-only
|
||||||
|
- Cannot add already-linked stores
|
||||||
|
- **Remove Stores**:
|
||||||
|
- Admin-only
|
||||||
|
- Cannot remove last store (validation)
|
||||||
|
- **Set Default Store**:
|
||||||
|
- Admin-only
|
||||||
|
- Default store loads automatically
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
- Viewers & Members: Read-only view of stores
|
||||||
|
- Admins: Full CRUD operations
|
||||||
|
|
||||||
|
### 4. Create/Join Household Modal
|
||||||
|
**Component**: [frontend/src/components/manage/CreateJoinHousehold.jsx](frontend/src/components/manage/CreateJoinHousehold.jsx)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Tabbed interface: "Create New" or "Join Existing"
|
||||||
|
- **Create Mode**:
|
||||||
|
- Enter household name
|
||||||
|
- Auto-generates invite code
|
||||||
|
- Creates household with user as admin
|
||||||
|
- **Join Mode**:
|
||||||
|
- Enter invite code
|
||||||
|
- Validates code and adds user as member
|
||||||
|
- Error handling for invalid codes
|
||||||
|
|
||||||
|
**Access**:
|
||||||
|
- Available from household switcher dropdown
|
||||||
|
- "+ Create or Join Household" button at bottom
|
||||||
|
- All authenticated users can access
|
||||||
|
|
||||||
|
### 5. Updated Household Switcher
|
||||||
|
**Component**: [frontend/src/components/household/HouseholdSwitcher.jsx](frontend/src/components/household/HouseholdSwitcher.jsx)
|
||||||
|
|
||||||
|
**Enhancements**:
|
||||||
|
- Added divider between household list and actions
|
||||||
|
- "+ Create or Join Household" button
|
||||||
|
- Opens CreateJoinHousehold modal
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
### CSS Files Created
|
||||||
|
1. **[frontend/src/styles/pages/Manage.css](frontend/src/styles/pages/Manage.css)**
|
||||||
|
- Page layout and tab navigation
|
||||||
|
- Responsive design
|
||||||
|
|
||||||
|
2. **[frontend/src/styles/components/manage/ManageHousehold.css](frontend/src/styles/components/manage/ManageHousehold.css)**
|
||||||
|
- Section cards with proper spacing
|
||||||
|
- Member cards with role badges
|
||||||
|
- Invite code display
|
||||||
|
- Danger zone styling
|
||||||
|
- Button styles (primary, secondary, danger)
|
||||||
|
|
||||||
|
3. **[frontend/src/styles/components/manage/ManageStores.css](frontend/src/styles/components/manage/ManageStores.css)**
|
||||||
|
- Grid layout for store cards
|
||||||
|
- Default badge styling
|
||||||
|
- Add store panel
|
||||||
|
- Available stores grid
|
||||||
|
|
||||||
|
4. **[frontend/src/styles/components/manage/CreateJoinHousehold.css](frontend/src/styles/components/manage/CreateJoinHousehold.css)**
|
||||||
|
- Modal overlay and container
|
||||||
|
- Mode tabs styling
|
||||||
|
- Form inputs and buttons
|
||||||
|
- Error message styling
|
||||||
|
|
||||||
|
### Theme Updates
|
||||||
|
**[frontend/src/styles/theme.css](frontend/src/styles/theme.css)**
|
||||||
|
|
||||||
|
Added simplified CSS variable aliases:
|
||||||
|
```css
|
||||||
|
--primary: var(--color-primary);
|
||||||
|
--primary-dark: var(--color-primary-dark);
|
||||||
|
--primary-light: var(--color-primary-light);
|
||||||
|
--danger: var(--color-danger);
|
||||||
|
--danger-dark: var(--color-danger-hover);
|
||||||
|
--text-primary: var(--color-text-primary);
|
||||||
|
--text-secondary: var(--color-text-secondary);
|
||||||
|
--background: var(--color-bg-body);
|
||||||
|
--border: var(--color-border-light);
|
||||||
|
--card-hover: var(--color-bg-hover);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Endpoints Used
|
||||||
|
|
||||||
|
All endpoints already existed - no backend changes required!
|
||||||
|
|
||||||
|
### Household Endpoints
|
||||||
|
- `GET /households` - Get user's households
|
||||||
|
- `POST /households` - Create household
|
||||||
|
- `PATCH /households/:id` - Update household name
|
||||||
|
- `DELETE /households/:id` - Delete household
|
||||||
|
- `POST /households/:id/invite/refresh` - Refresh invite code
|
||||||
|
- `POST /households/join/:inviteCode` - Join via invite code
|
||||||
|
- `GET /households/:id/members` - Get members
|
||||||
|
- `PATCH /households/:id/members/:userId/role` - Update member role
|
||||||
|
- `DELETE /households/:id/members/:userId` - Remove member
|
||||||
|
|
||||||
|
### Store Endpoints
|
||||||
|
- `GET /stores` - Get all stores
|
||||||
|
- `GET /stores/household/:householdId` - Get household stores
|
||||||
|
- `POST /stores/household/:householdId` - Add store to household
|
||||||
|
- `DELETE /stores/household/:householdId/:storeId` - Remove store
|
||||||
|
- `PATCH /stores/household/:householdId/:storeId/default` - Set default
|
||||||
|
|
||||||
|
## User Flow
|
||||||
|
|
||||||
|
### Managing Household
|
||||||
|
1. Click "Manage" in navbar
|
||||||
|
2. View household overview (name, members, invite code)
|
||||||
|
3. As admin:
|
||||||
|
- Edit household name
|
||||||
|
- Generate new invite codes
|
||||||
|
- Promote/demote members
|
||||||
|
- Remove members
|
||||||
|
- Delete household (danger zone)
|
||||||
|
|
||||||
|
### Managing Stores
|
||||||
|
1. Click "Manage" in navbar
|
||||||
|
2. Click "Stores" tab
|
||||||
|
3. View all linked stores with default badge
|
||||||
|
4. As admin:
|
||||||
|
- Click "+ Add Store" to see available stores
|
||||||
|
- Click "Add" on any unlinked store
|
||||||
|
- Click "Set as Default" on non-default stores
|
||||||
|
- Click "Remove" to unlink store (except last one)
|
||||||
|
|
||||||
|
### Creating/Joining Household
|
||||||
|
1. Click household name in navbar
|
||||||
|
2. Click "+ Create or Join Household" at bottom of dropdown
|
||||||
|
3. Select "Create New" or "Join Existing" tab
|
||||||
|
4. Fill form and submit
|
||||||
|
5. New household appears in list and becomes active
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
|
||||||
|
All components are fully responsive:
|
||||||
|
- **Desktop**: Grid layouts, side-by-side buttons
|
||||||
|
- **Tablet**: Adjusted spacing, smaller grids
|
||||||
|
- **Mobile**:
|
||||||
|
- Single column layouts
|
||||||
|
- Full-width buttons
|
||||||
|
- Stacked form elements
|
||||||
|
- Optimized spacing
|
||||||
|
|
||||||
|
## Permissions Summary
|
||||||
|
|
||||||
|
| Feature | Viewer | Member | Admin |
|
||||||
|
|---------|--------|--------|-------|
|
||||||
|
| View household info | ✅ | ✅ | ✅ |
|
||||||
|
| Edit household name | ❌ | ❌ | ✅ |
|
||||||
|
| View invite code | ❌ | ❌ | ✅ |
|
||||||
|
| Refresh invite code | ❌ | ❌ | ✅ |
|
||||||
|
| View members | ✅ | ✅ | ✅ |
|
||||||
|
| Change member roles | ❌ | ❌ | ✅ |
|
||||||
|
| Remove members | ❌ | ❌ | ✅ |
|
||||||
|
| Delete household | ❌ | ❌ | ✅ |
|
||||||
|
| View stores | ✅ | ✅ | ✅ |
|
||||||
|
| Add stores | ❌ | ❌ | ✅ |
|
||||||
|
| Remove stores | ❌ | ❌ | ✅ |
|
||||||
|
| Set default store | ❌ | ❌ | ✅ |
|
||||||
|
| Create household | ✅ | ✅ | ✅ |
|
||||||
|
| Join household | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Consider adding:
|
||||||
|
1. **Household Settings**: Description, profile image, preferences
|
||||||
|
2. **Member Invitations**: Direct user search instead of just invite codes
|
||||||
|
3. **Store Details**: View item counts, last activity per store
|
||||||
|
4. **Audit Log**: Track household/store changes
|
||||||
|
5. **Notifications**: Member added/removed, role changes
|
||||||
|
6. **Bulk Operations**: Remove multiple members at once
|
||||||
|
7. **Store Categories**: Group stores by region/type
|
||||||
|
8. **Export Data**: Download household grocery history
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Create new household and verify admin role
|
||||||
|
- [ ] Generate and copy invite code
|
||||||
|
- [ ] Join household using invite code
|
||||||
|
- [ ] Edit household name as admin
|
||||||
|
- [ ] Promote member to admin
|
||||||
|
- [ ] Demote admin to member
|
||||||
|
- [ ] Remove member from household
|
||||||
|
- [ ] Add store to household
|
||||||
|
- [ ] Set default store
|
||||||
|
- [ ] Remove store (verify last store protection)
|
||||||
|
- [ ] Try admin actions as non-admin (should be hidden/disabled)
|
||||||
|
- [ ] Delete household and verify redirect
|
||||||
|
- [ ] Test responsive layouts on mobile/tablet/desktop
|
||||||
|
- [ ] Verify all error messages display properly
|
||||||
|
- [ ] Test with multiple households
|
||||||
@ -40,5 +40,5 @@ exports.login = async (req, res) => {
|
|||||||
{ expiresIn: "1 year" }
|
{ expiresIn: "1 year" }
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ token, username, role: user.role });
|
res.json({ token, userId: user.id, username, role: user.role });
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { StoreProvider } from "./context/StoreContext.jsx";
|
|||||||
import AdminPanel from "./pages/AdminPanel.jsx";
|
import AdminPanel from "./pages/AdminPanel.jsx";
|
||||||
import GroceryList from "./pages/GroceryList.jsx";
|
import GroceryList from "./pages/GroceryList.jsx";
|
||||||
import Login from "./pages/Login.jsx";
|
import Login from "./pages/Login.jsx";
|
||||||
|
import Manage from "./pages/Manage.jsx";
|
||||||
import Register from "./pages/Register.jsx";
|
import Register from "./pages/Register.jsx";
|
||||||
import Settings from "./pages/Settings.jsx";
|
import Settings from "./pages/Settings.jsx";
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path="/" element={<GroceryList />} />
|
<Route path="/" element={<GroceryList />} />
|
||||||
|
<Route path="/manage" element={<Manage />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { HouseholdContext } from '../../context/HouseholdContext';
|
import { HouseholdContext } from '../../context/HouseholdContext';
|
||||||
import '../../styles/components/HouseholdSwitcher.css';
|
import '../../styles/components/HouseholdSwitcher.css';
|
||||||
|
import CreateJoinHousehold from '../manage/CreateJoinHousehold';
|
||||||
|
|
||||||
export default function HouseholdSwitcher() {
|
export default function HouseholdSwitcher() {
|
||||||
const { households, activeHousehold, setActiveHousehold, loading } = useContext(HouseholdContext);
|
const { households, activeHousehold, setActiveHousehold, loading } = useContext(HouseholdContext);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [showCreateJoin, setShowCreateJoin] = useState(false);
|
||||||
|
|
||||||
if (!activeHousehold || households.length === 0) {
|
if (!activeHousehold || households.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -42,9 +44,23 @@ export default function HouseholdSwitcher() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<div className="household-divider"></div>
|
||||||
|
<button
|
||||||
|
className="household-option create-household-btn"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setShowCreateJoin(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Create or Join Household
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showCreateJoin && (
|
||||||
|
<CreateJoinHousehold onClose={() => setShowCreateJoin(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export default function Navbar() {
|
|||||||
<nav className="navbar">
|
<nav className="navbar">
|
||||||
<div className="navbar-links">
|
<div className="navbar-links">
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
|
<Link to="/manage">Manage</Link>
|
||||||
<Link to="/settings">Settings</Link>
|
<Link to="/settings">Settings</Link>
|
||||||
|
|
||||||
{role === "admin" && <Link to="/admin">Admin</Link>}
|
{role === "admin" && <Link to="/admin">Admin</Link>}
|
||||||
|
|||||||
130
frontend/src/components/manage/CreateJoinHousehold.jsx
Normal file
130
frontend/src/components/manage/CreateJoinHousehold.jsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { createHousehold, joinHousehold } from "../../api/households";
|
||||||
|
import { HouseholdContext } from "../../context/HouseholdContext";
|
||||||
|
import "../../styles/components/manage/CreateJoinHousehold.css";
|
||||||
|
|
||||||
|
export default function CreateJoinHousehold({ onClose }) {
|
||||||
|
const { loadHouseholds } = useContext(HouseholdContext);
|
||||||
|
const [mode, setMode] = useState("create"); // "create" or "join"
|
||||||
|
const [householdName, setHouseholdName] = useState("");
|
||||||
|
const [inviteCode, setInviteCode] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleCreate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!householdName.trim()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createHousehold(householdName);
|
||||||
|
await loadHouseholds();
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to create household:", err);
|
||||||
|
setError(err.response?.data?.message || "Failed to create household");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoin = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!inviteCode.trim()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await joinHousehold(inviteCode);
|
||||||
|
await loadHouseholds();
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to join household:", err);
|
||||||
|
setError(err.response?.data?.message || "Failed to join household");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="create-join-modal-overlay" onClick={onClose}>
|
||||||
|
<div className="create-join-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Household</h2>
|
||||||
|
<button className="close-btn" onClick={onClose}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mode-tabs">
|
||||||
|
<button
|
||||||
|
className={`mode-tab ${mode === "create" ? "active" : ""}`}
|
||||||
|
onClick={() => setMode("create")}
|
||||||
|
>
|
||||||
|
Create New
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`mode-tab ${mode === "join" ? "active" : ""}`}
|
||||||
|
onClick={() => setMode("join")}
|
||||||
|
>
|
||||||
|
Join Existing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error-message">{error}</div>}
|
||||||
|
|
||||||
|
{mode === "create" ? (
|
||||||
|
<form onSubmit={handleCreate} className="household-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="householdName">Household Name</label>
|
||||||
|
<input
|
||||||
|
id="householdName"
|
||||||
|
type="text"
|
||||||
|
value={householdName}
|
||||||
|
onChange={(e) => setHouseholdName(e.target.value)}
|
||||||
|
placeholder="e.g., Smith Family"
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" onClick={onClose} className="btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn-primary" disabled={loading}>
|
||||||
|
{loading ? "Creating..." : "Create Household"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleJoin} className="household-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="inviteCode">Invite Code</label>
|
||||||
|
<input
|
||||||
|
id="inviteCode"
|
||||||
|
type="text"
|
||||||
|
value={inviteCode}
|
||||||
|
onChange={(e) => setInviteCode(e.target.value)}
|
||||||
|
placeholder="Enter invite code"
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<p className="form-hint">
|
||||||
|
Ask the household admin for the invite code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" onClick={onClose} className="btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn-primary" disabled={loading}>
|
||||||
|
{loading ? "Joining..." : "Join Household"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
229
frontend/src/components/manage/ManageHousehold.jsx
Normal file
229
frontend/src/components/manage/ManageHousehold.jsx
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
deleteHousehold,
|
||||||
|
getHouseholdMembers,
|
||||||
|
refreshInviteCode,
|
||||||
|
removeMember,
|
||||||
|
updateHousehold,
|
||||||
|
updateMemberRole
|
||||||
|
} from "../../api/households";
|
||||||
|
import { AuthContext } from "../../context/AuthContext";
|
||||||
|
import { HouseholdContext } from "../../context/HouseholdContext";
|
||||||
|
import "../../styles/components/manage/ManageHousehold.css";
|
||||||
|
|
||||||
|
export default function ManageHousehold() {
|
||||||
|
const { userId } = useContext(AuthContext);
|
||||||
|
const { activeHousehold, refreshHouseholds } = useContext(HouseholdContext);
|
||||||
|
const [members, setMembers] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editingName, setEditingName] = useState(false);
|
||||||
|
const [newName, setNewName] = useState("");
|
||||||
|
const [showInviteCode, setShowInviteCode] = useState(false);
|
||||||
|
|
||||||
|
const isAdmin = activeHousehold?.role === "admin";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadMembers();
|
||||||
|
}, [activeHousehold?.id]);
|
||||||
|
|
||||||
|
const loadMembers = async () => {
|
||||||
|
if (!activeHousehold?.id) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getHouseholdMembers(activeHousehold.id);
|
||||||
|
setMembers(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load members:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateName = async () => {
|
||||||
|
if (!newName.trim() || newName === activeHousehold.name) {
|
||||||
|
setEditingName(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Updating household:', activeHousehold.id, 'with name:', newName);
|
||||||
|
const response = await updateHousehold(activeHousehold.id, newName);
|
||||||
|
console.log('Update response:', response);
|
||||||
|
await refreshHouseholds();
|
||||||
|
setEditingName(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update household name:", error);
|
||||||
|
console.error("Error response:", error.response?.data);
|
||||||
|
alert(`Failed to update household name: ${error.response?.data?.error || error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshInvite = async () => {
|
||||||
|
if (!confirm("Generate a new invite code? The old code will no longer work.")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await refreshInviteCode(activeHousehold.id);
|
||||||
|
await refreshHouseholds();
|
||||||
|
alert(`New invite code: ${response.data.inviteCode}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to refresh invite code:", error);
|
||||||
|
alert("Failed to refresh invite code");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateRole = async (userId, currentRole) => {
|
||||||
|
const newRole = currentRole === "admin" ? "member" : "admin";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateMemberRole(activeHousehold.id, userId, newRole);
|
||||||
|
await loadMembers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update role:", error);
|
||||||
|
alert("Failed to update member role");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveMember = async (userId, username) => {
|
||||||
|
if (!confirm(`Remove ${username} from this household?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeMember(activeHousehold.id, userId);
|
||||||
|
await loadMembers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove member:", error);
|
||||||
|
alert("Failed to remove member");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteHousehold = async () => {
|
||||||
|
if (!confirm(`Delete "${activeHousehold.name}"? This will delete all lists and data. This cannot be undone.`)) return;
|
||||||
|
if (!confirm("Are you absolutely sure? Type DELETE to confirm.")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteHousehold(activeHousehold.id);
|
||||||
|
await refreshHouseholds();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete household:", error);
|
||||||
|
alert("Failed to delete household");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyInviteCode = () => {
|
||||||
|
navigator.clipboard.writeText(activeHousehold.invite_code);
|
||||||
|
alert("Invite code copied to clipboard!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="manage-household">
|
||||||
|
{/* Household Name Section */}
|
||||||
|
<section className="manage-section">
|
||||||
|
<h2>Household Name</h2>
|
||||||
|
{editingName ? (
|
||||||
|
<div className="edit-name-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
placeholder="Household name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button onClick={handleUpdateName} className="btn-primary">Save</button>
|
||||||
|
<button onClick={() => setEditingName(false)} className="btn-secondary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="name-display">
|
||||||
|
<h3>{activeHousehold.name}</h3>
|
||||||
|
{isAdmin && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setNewName(activeHousehold.name);
|
||||||
|
setEditingName(true);
|
||||||
|
}}
|
||||||
|
className="btn-secondary"
|
||||||
|
>
|
||||||
|
Edit Name
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Invite Code Section */}
|
||||||
|
{isAdmin && (
|
||||||
|
<section className="manage-section">
|
||||||
|
<h2>Invite Code</h2>
|
||||||
|
<p className="section-description">
|
||||||
|
Share this code with others to invite them to your household.
|
||||||
|
</p>
|
||||||
|
<div className="invite-actions">
|
||||||
|
<button onClick={() => setShowInviteCode(!showInviteCode)} className="btn-secondary">
|
||||||
|
{showInviteCode ? "Hide Code" : "Show Code"}
|
||||||
|
</button>
|
||||||
|
{showInviteCode && (
|
||||||
|
<>
|
||||||
|
<code className="invite-code">{activeHousehold.invite_code}</code>
|
||||||
|
<button onClick={copyInviteCode} className="btn-secondary">Copy</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button onClick={handleRefreshInvite} className="btn-secondary">
|
||||||
|
Generate New Code
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Members Section */}
|
||||||
|
<section className="manage-section">
|
||||||
|
<h2>Members ({members.length})</h2>
|
||||||
|
{loading ? (
|
||||||
|
<p>Loading members...</p>
|
||||||
|
) : (
|
||||||
|
<div className="members-list">
|
||||||
|
{members.map((member) => (
|
||||||
|
<div key={member.user_id} className="member-card">
|
||||||
|
<div className="member-info">
|
||||||
|
<span className="member-name">
|
||||||
|
{member.username}
|
||||||
|
{member.user_id === parseInt(userId) && " (You)"}
|
||||||
|
</span>
|
||||||
|
<span className={`member-role ${member.role}`}>
|
||||||
|
{member.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isAdmin && member.user_id !== parseInt(userId) && (
|
||||||
|
<div className="member-actions">
|
||||||
|
<button
|
||||||
|
onClick={() => handleUpdateRole(member.user_id, member.role)}
|
||||||
|
className="btn-secondary btn-small"
|
||||||
|
>
|
||||||
|
{member.role === "admin" ? "Make Member" : "Make Admin"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveMember(member.user_id, member.username)}
|
||||||
|
className="btn-danger btn-small"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Danger Zone */}
|
||||||
|
{isAdmin && (
|
||||||
|
<section className="manage-section danger-zone">
|
||||||
|
<h2>Danger Zone</h2>
|
||||||
|
<p className="section-description">
|
||||||
|
Deleting a household is permanent and will delete all lists, items, and history.
|
||||||
|
</p>
|
||||||
|
<button onClick={handleDeleteHousehold} className="btn-danger">
|
||||||
|
Delete Household
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
frontend/src/components/manage/ManageStores.jsx
Normal file
157
frontend/src/components/manage/ManageStores.jsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
addStoreToHousehold,
|
||||||
|
getAllStores,
|
||||||
|
removeStoreFromHousehold,
|
||||||
|
setDefaultStore
|
||||||
|
} from "../../api/stores";
|
||||||
|
import { HouseholdContext } from "../../context/HouseholdContext";
|
||||||
|
import { StoreContext } from "../../context/StoreContext";
|
||||||
|
import "../../styles/components/manage/ManageStores.css";
|
||||||
|
|
||||||
|
export default function ManageStores() {
|
||||||
|
const { activeHousehold } = useContext(HouseholdContext);
|
||||||
|
const { stores: householdStores, loadStores } = useContext(StoreContext);
|
||||||
|
const [allStores, setAllStores] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showAddStore, setShowAddStore] = useState(false);
|
||||||
|
|
||||||
|
const isAdmin = activeHousehold?.role === "admin";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAllStores();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadAllStores = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getAllStores();
|
||||||
|
setAllStores(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load stores:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddStore = async (storeId) => {
|
||||||
|
try {
|
||||||
|
await addStoreToHousehold(activeHousehold.id, storeId, false);
|
||||||
|
await loadStores();
|
||||||
|
setShowAddStore(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add store:", error);
|
||||||
|
alert("Failed to add store");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveStore = async (storeId, storeName) => {
|
||||||
|
if (!confirm(`Remove ${storeName} from this household?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeStoreFromHousehold(activeHousehold.id, storeId);
|
||||||
|
await loadStores();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove store:", error);
|
||||||
|
alert("Failed to remove store");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetDefault = async (storeId) => {
|
||||||
|
try {
|
||||||
|
await setDefaultStore(activeHousehold.id, storeId);
|
||||||
|
await loadStores();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to set default store:", error);
|
||||||
|
alert("Failed to set default store");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableStores = allStores.filter(
|
||||||
|
store => !householdStores.some(hs => hs.id === store.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="manage-stores">
|
||||||
|
{/* Current Stores Section */}
|
||||||
|
<section className="manage-section">
|
||||||
|
<h2>Your Stores ({householdStores.length})</h2>
|
||||||
|
{householdStores.length === 0 ? (
|
||||||
|
<p className="empty-message">No stores added yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="stores-list">
|
||||||
|
{householdStores.map((store) => (
|
||||||
|
<div key={store.id} className="store-card">
|
||||||
|
<div className="store-info">
|
||||||
|
<h3>{store.name}</h3>
|
||||||
|
{store.location && <p className="store-location">{store.location}</p>}
|
||||||
|
{store.is_default && <span className="default-badge">Default</span>}
|
||||||
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="store-actions">
|
||||||
|
{!store.is_default && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSetDefault(store.id)}
|
||||||
|
className="btn-secondary btn-small"
|
||||||
|
>
|
||||||
|
Set as Default
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveStore(store.id, store.name)}
|
||||||
|
className="btn-danger btn-small"
|
||||||
|
disabled={householdStores.length === 1}
|
||||||
|
title={householdStores.length === 1 ? "Cannot remove last store" : ""}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Add Store Section */}
|
||||||
|
{isAdmin && (
|
||||||
|
<section className="manage-section">
|
||||||
|
<h2>Add Store</h2>
|
||||||
|
{!showAddStore ? (
|
||||||
|
<button onClick={() => setShowAddStore(true)} className="btn-primary">
|
||||||
|
+ Add Store
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="add-store-panel">
|
||||||
|
<button onClick={() => setShowAddStore(false)} className="btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{loading ? (
|
||||||
|
<p>Loading stores...</p>
|
||||||
|
) : availableStores.length === 0 ? (
|
||||||
|
<p className="empty-message">All available stores have been added.</p>
|
||||||
|
) : (
|
||||||
|
<div className="available-stores">
|
||||||
|
{availableStores.map((store) => (
|
||||||
|
<div key={store.id} className="available-store-card">
|
||||||
|
<div className="store-info">
|
||||||
|
<h3>{store.name}</h3>
|
||||||
|
{store.location && <p className="store-location">{store.location}</p>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddStore(store.id)}
|
||||||
|
className="btn-primary btn-small"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
frontend/src/components/manage/index.js
Normal file
4
frontend/src/components/manage/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as CreateJoinHousehold } from './CreateJoinHousehold';
|
||||||
|
export { default as ManageHousehold } from './ManageHousehold';
|
||||||
|
export { default as ManageStores } from './ManageStores';
|
||||||
|
|
||||||
@ -2,6 +2,7 @@ import { createContext, useState } from 'react';
|
|||||||
|
|
||||||
export const AuthContext = createContext({
|
export const AuthContext = createContext({
|
||||||
token: null,
|
token: null,
|
||||||
|
userId: null,
|
||||||
role: null,
|
role: null,
|
||||||
username: null,
|
username: null,
|
||||||
login: () => { },
|
login: () => { },
|
||||||
@ -10,14 +11,17 @@ export const AuthContext = createContext({
|
|||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
const [token, setToken] = useState(localStorage.getItem('token') || null);
|
const [token, setToken] = useState(localStorage.getItem('token') || null);
|
||||||
|
const [userId, setUserId] = useState(localStorage.getItem('userId') || null);
|
||||||
const [role, setRole] = useState(localStorage.getItem('role') || null);
|
const [role, setRole] = useState(localStorage.getItem('role') || null);
|
||||||
const [username, setUsername] = useState(localStorage.getItem('username') || null);
|
const [username, setUsername] = useState(localStorage.getItem('username') || null);
|
||||||
|
|
||||||
const login = (data) => {
|
const login = (data) => {
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
|
localStorage.setItem('userId', data.userId);
|
||||||
localStorage.setItem('role', data.role);
|
localStorage.setItem('role', data.role);
|
||||||
localStorage.setItem('username', data.username);
|
localStorage.setItem('username', data.username);
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
|
setUserId(data.userId);
|
||||||
setRole(data.role);
|
setRole(data.role);
|
||||||
setUsername(data.username);
|
setUsername(data.username);
|
||||||
};
|
};
|
||||||
@ -26,12 +30,14 @@ export const AuthProvider = ({ children }) => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
setUserId(null);
|
||||||
setRole(null);
|
setRole(null);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
token,
|
token,
|
||||||
|
userId,
|
||||||
role,
|
role,
|
||||||
username,
|
username,
|
||||||
login,
|
login,
|
||||||
|
|||||||
51
frontend/src/pages/Manage.jsx
Normal file
51
frontend/src/pages/Manage.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import ManageHousehold from "../components/manage/ManageHousehold";
|
||||||
|
import ManageStores from "../components/manage/ManageStores";
|
||||||
|
import { HouseholdContext } from "../context/HouseholdContext";
|
||||||
|
import "../styles/pages/Manage.css";
|
||||||
|
|
||||||
|
export default function Manage() {
|
||||||
|
const { activeHousehold } = useContext(HouseholdContext);
|
||||||
|
const [activeTab, setActiveTab] = useState("household");
|
||||||
|
|
||||||
|
if (!activeHousehold) {
|
||||||
|
return (
|
||||||
|
<div className="manage-body">
|
||||||
|
<div className="manage-container">
|
||||||
|
<h1 className="manage-title">Manage</h1>
|
||||||
|
<p style={{ textAlign: 'center', marginTop: '2rem', color: 'var(--text-secondary)' }}>
|
||||||
|
Loading household...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="manage-body">
|
||||||
|
<div className="manage-container">
|
||||||
|
<h1 className="manage-title">Manage {activeHousehold.name}</h1>
|
||||||
|
|
||||||
|
<div className="manage-tabs">
|
||||||
|
<button
|
||||||
|
className={`manage-tab ${activeTab === "household" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("household")}
|
||||||
|
>
|
||||||
|
Household
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`manage-tab ${activeTab === "stores" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("stores")}
|
||||||
|
>
|
||||||
|
Stores
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="manage-content">
|
||||||
|
{activeTab === "household" && <ManageHousehold />}
|
||||||
|
{activeTab === "stores" && <ManageStores />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,18 +8,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: var(--surface-color);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: 6px;
|
||||||
color: var(--text-color);
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.household-switcher-toggle:hover {
|
.household-switcher-toggle:hover {
|
||||||
background: var(--hover-color);
|
background: var(--card-hover);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.household-switcher-toggle:disabled {
|
.household-switcher-toggle:disabled {
|
||||||
@ -53,11 +53,11 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.5rem);
|
top: calc(100% + 0.5rem);
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 200px;
|
min-width: 220px;
|
||||||
background: var(--surface-color);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 2px solid var(--border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -67,15 +67,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.875rem 1rem;
|
||||||
background: transparent;
|
background: var(--card-bg);
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border);
|
||||||
color: var(--text-color);
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.household-option:last-child {
|
.household-option:last-child {
|
||||||
@ -83,16 +83,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.household-option:hover {
|
.household-option:hover {
|
||||||
background: var(--hover-color);
|
background: var(--card-hover);
|
||||||
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.household-option.active {
|
.household-option.active {
|
||||||
background: var(--primary-color-light);
|
background: rgba(30, 144, 255, 0.15);
|
||||||
color: var(--primary-color);
|
color: var(--primary);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-mark {
|
.check-mark {
|
||||||
color: var(--primary-color);
|
color: var(--primary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.household-divider {);
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-household-btn {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-household-btn:hover {
|
||||||
|
background: rgba(30, 144, 255, 0.15
|
||||||
|
.create-household-btn:hover {
|
||||||
|
background: var(--primary-color-light);
|
||||||
}
|
}
|
||||||
|
|||||||
165
frontend/src/styles/components/manage/CreateJoinHousehold.css
Normal file
165
frontend/src/styles/components/manage/CreateJoinHousehold.css
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/* Create/Join Household Modal */
|
||||||
|
.create-join-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-join-modal {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mode Tabs */
|
||||||
|
.mode-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab.active {
|
||||||
|
color: var(--primary);
|
||||||
|
border-bottom-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.household-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin: 1rem 1.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--danger-light, rgba(220, 53, 69, 0.1));
|
||||||
|
border: 1px solid var(--danger);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.create-join-modal {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
max-height: 100vh;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
252
frontend/src/styles/components/manage/ManageHousehold.css
Normal file
252
frontend/src/styles/components/manage/ManageHousehold.css
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
/* Manage Household Component */
|
||||||
|
.manage-household {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Styling */
|
||||||
|
.manage-section {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-section h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Household Name Section */
|
||||||
|
.name-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-display h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-name-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-name-form input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invite Code Section */
|
||||||
|
.invite-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-code {
|
||||||
|
background: var(--background);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--primary);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Members Section */
|
||||||
|
.members-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card:hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role.admin {
|
||||||
|
background: var(--primary-light, rgba(0, 122, 255, 0.1));
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role.member {
|
||||||
|
background: var(--text-secondary-light, rgba(128, 128, 128, 0.1));
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger Zone */
|
||||||
|
.danger-zone {
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone h2 {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary,
|
||||||
|
.btn-danger {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--primary-dark, #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: var(--danger-dark, #c82333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:disabled {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.manage-section {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-display {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-name-form {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-name-form input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-name-form button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-actions button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-code {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
161
frontend/src/styles/components/manage/ManageStores.css
Normal file
161
frontend/src/styles/components/manage/ManageStores.css
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/* Manage Stores Component */
|
||||||
|
.manage-stores {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Styling */
|
||||||
|
.manage-section {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-section h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stores List */
|
||||||
|
.stores-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card {
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-info h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-location {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Store Panel */
|
||||||
|
.add-store-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-stores {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card {
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card .store-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card .store-location {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-message {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.stores-list,
|
||||||
|
.available-stores {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-actions button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-store-card button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
frontend/src/styles/pages/Manage.css
Normal file
119
frontend/src/styles/pages/Manage.css
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/* Manage Page Layout */
|
||||||
|
.manage-body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--background);
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-container {
|
||||||
|
max-width: 850px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.manage-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-bottom: 2px solid var(--border);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-tab {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-tab:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-tab.active {
|
||||||
|
color: var(--primary);
|
||||||
|
border-bottom-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.manage-content {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--primary-dark, #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.manage-body {
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-tab {
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.manage-body {
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-tab {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,10 +14,10 @@
|
|||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/* Primary Colors */
|
/* Primary Colors */
|
||||||
--color-primary: #007bff;
|
--color-primary: dodgerblue;
|
||||||
--color-primary-hover: #0056b3;
|
--color-primary-hover: #0066cc;
|
||||||
--color-primary-light: #e7f3ff;
|
--color-primary-light: #e7f3ff;
|
||||||
--color-primary-dark: #0067d8;
|
--color-primary-dark: #0056b3;
|
||||||
|
|
||||||
/* Secondary Colors */
|
/* Secondary Colors */
|
||||||
--color-secondary: #6c757d;
|
--color-secondary: #6c757d;
|
||||||
@ -187,6 +187,20 @@
|
|||||||
--modal-border-radius: var(--border-radius-lg);
|
--modal-border-radius: var(--border-radius-lg);
|
||||||
--modal-padding: var(--spacing-lg);
|
--modal-padding: var(--spacing-lg);
|
||||||
--modal-max-width: 500px;
|
--modal-max-width: 500px;
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SIMPLIFIED ALIASES (for component convenience)
|
||||||
|
============================================ */
|
||||||
|
--primary: var(--color-primary);
|
||||||
|
--primary-dark: var(--color-primary-dark);
|
||||||
|
--primary-light: var(--color-primary-light);
|
||||||
|
--danger: var(--color-danger);
|
||||||
|
--danger-dark: var(--color-danger-hover);
|
||||||
|
--text-primary: var(--color-text-primary);
|
||||||
|
--text-secondary: var(--color-text-secondary);
|
||||||
|
--background: var(--color-bg-body);
|
||||||
|
--border: var(--color-border-light);
|
||||||
|
--card-hover: var(--color-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -109,12 +109,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--color-secondary);
|
background: var(--color-primary);
|
||||||
color: var(--color-text-inverse);
|
color: var(--color-text-inverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
.btn-secondary:hover:not(:disabled) {
|
||||||
background: var(--color-secondary-hover);
|
background: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user