diff --git a/frontend/COMPONENT_STRUCTURE.md b/frontend/COMPONENT_STRUCTURE.md new file mode 100644 index 0000000..4fca22d --- /dev/null +++ b/frontend/COMPONENT_STRUCTURE.md @@ -0,0 +1,238 @@ +# Frontend Component Organization + +This document describes the organized structure of the frontend codebase, implemented to improve maintainability as the application grows. + +## Directory Structure + +``` +frontend/src/ +├── api/ # API client functions +│ ├── auth.js # Authentication endpoints +│ ├── axios.js # Axios instance with interceptors +│ ├── list.js # Grocery list endpoints +│ └── users.js # User management endpoints +│ +├── components/ # React components (organized by function) +│ ├── common/ # Reusable UI components +│ │ ├── ErrorMessage.jsx +│ │ ├── FloatingActionButton.jsx +│ │ ├── FormInput.jsx +│ │ ├── SortDropdown.jsx +│ │ ├── UserRoleCard.jsx +│ │ └── index.js # Barrel exports +│ │ +│ ├── modals/ # All modal/dialog components +│ │ ├── AddImageModal.jsx +│ │ ├── AddItemWithDetailsModal.jsx +│ │ ├── ConfirmBuyModal.jsx +│ │ ├── EditItemModal.jsx +│ │ ├── ImageModal.jsx +│ │ ├── ImageUploadModal.jsx +│ │ ├── ItemClassificationModal.jsx +│ │ ├── SimilarItemModal.jsx +│ │ └── index.js # Barrel exports +│ │ +│ ├── forms/ # Form components and input sections +│ │ ├── AddItemForm.jsx +│ │ ├── ClassificationSection.jsx +│ │ ├── ImageUploadSection.jsx +│ │ └── index.js # Barrel exports +│ │ +│ ├── items/ # Item display and list components +│ │ ├── GroceryItem.tsx +│ │ ├── GroceryListItem.jsx +│ │ ├── SuggestionList.tsx +│ │ └── index.js # Barrel exports +│ │ +│ └── layout/ # Layout and navigation components +│ ├── AppLayout.jsx +│ ├── Navbar.jsx +│ └── index.js # Barrel exports +│ +├── constants/ # Application constants +│ ├── classifications.js # Item types, groups, zones +│ └── roles.js # User roles (viewer, editor, admin) +│ +├── context/ # React context providers +│ └── AuthContext.jsx # Authentication context +│ +├── pages/ # Top-level page components +│ ├── AdminPanel.jsx # User management dashboard +│ ├── GroceryList.jsx # Main grocery list page +│ ├── Login.jsx # Login page +│ └── Register.jsx # Registration page +│ +├── styles/ # CSS files (organized by type) +│ ├── pages/ # Page-specific styles +│ │ ├── GroceryList.css +│ │ ├── Login.css +│ │ └── Register.css +│ │ +│ ├── components/ # Component-specific styles +│ │ ├── AddItemWithDetailsModal.css +│ │ ├── ClassificationSection.css +│ │ ├── EditItemModal.css +│ │ ├── ImageUploadSection.css +│ │ └── Navbar.css +│ │ +│ ├── theme.css # **GLOBAL THEME VARIABLES** (colors, spacing, typography) +│ ├── THEME_USAGE_EXAMPLES.css # Examples of using theme variables +│ ├── App.css # Global app styles +│ └── index.css # Root styles (uses theme variables) +│ +├── utils/ # Utility functions +│ ├── PrivateRoute.jsx # Authentication guard +│ ├── RoleGuard.jsx # Role-based access guard +│ └── stringSimilarity.js # String matching utilities +│ +├── App.jsx # Root app component +├── main.tsx # Application entry point +├── config.ts # Configuration (API URL) +└── types.ts # TypeScript type definitions +``` + +## Import Patterns + +### Using Barrel Exports (Recommended) + +Barrel exports (`index.js` files) allow cleaner imports from component groups: + +```javascript +// ✅ Clean barrel import +import { FloatingActionButton, SortDropdown } from '../components/common'; +import { EditItemModal, SimilarItemModal } from '../components/modals'; +import { AddItemForm, ClassificationSection } from '../components/forms'; +``` + +### Direct Imports (Alternative) + +You can also import components directly when needed: + +```javascript +// Also valid +import FloatingActionButton from '../components/common/FloatingActionButton'; +import EditItemModal from '../components/modals/EditItemModal'; +``` + +## Component Categories + +### `common/` - Reusable UI Components +- **Purpose**: Generic, reusable components used across multiple pages +- **Examples**: Buttons, dropdowns, form inputs, error messages +- **Characteristics**: Highly reusable, minimal business logic + +### `modals/` - Dialog Components +- **Purpose**: All modal/dialog/popup components +- **Examples**: Confirmation dialogs, edit forms, image viewers +- **Characteristics**: Overlay UI, typically used for focused interactions + +### `forms/` - Form Sections +- **Purpose**: Form-related components and input sections +- **Examples**: Multi-step forms, reusable form sections +- **Characteristics**: Handle user input, validation, form state + +### `items/` - Item Display Components +- **Purpose**: Components specific to displaying grocery items +- **Examples**: Item cards, item lists, suggestion lists +- **Characteristics**: Domain-specific (grocery items) + +### `layout/` - Layout Components +- **Purpose**: Application structure and navigation +- **Examples**: Navigation bars, page layouts, wrappers +- **Characteristics**: Define page structure, persistent UI elements + +## Style Organization + +### `styles/pages/` +Page-specific styles that apply to entire page components. + +### `styles/components/` +Component-specific styles for individual reusable components. + +## Benefits of This Structure + +1. **Scalability**: Easy to add new components without cluttering directories +2. **Discoverability**: Intuitive naming makes components easy to find +3. **Maintainability**: Related code is grouped together +4. **Separation of Concerns**: Clear boundaries between different types of components +5. **Import Clarity**: Barrel exports reduce import statement complexity +6. **Team Collaboration**: Clear conventions for where new code should go + +## Adding New Components + +When adding a new component, ask: + +1. **Is it reusable across pages?** → `common/` +2. **Is it a modal/dialog?** → `modals/` +3. **Is it form-related?** → `forms/` +4. **Is it specific to grocery items?** → `items/` +5. **Does it define page structure?** → `layout/` +6. **Is it a full page?** → `pages/` + +Then: +1. Create the component in the appropriate subdirectory +2. Add the component to the subdirectory's `index.js` barrel export +3. Create corresponding CSS file in `styles/pages/` or `styles/components/` + +## Migration Notes + +This structure was implemented on January 2, 2026 to organize 20+ components and 10+ CSS files that were previously in flat directories. All import paths have been updated to reflect the new structure. + +## Theming System + +The application uses a centralized theming system via CSS custom properties (variables) defined in `styles/theme.css`. + +### Theme Variables + +All design tokens are defined in `theme.css` including: + +- **Colors**: Primary, secondary, semantic (success, danger, warning), neutrals +- **Spacing**: Consistent spacing scale (xs, sm, md, lg, xl, 2xl, 3xl) +- **Typography**: Font families, sizes, weights, line heights +- **Borders**: Widths and radius values +- **Shadows**: Box shadow presets +- **Transitions**: Timing functions +- **Z-index**: Layering system for modals, dropdowns, etc. + +### Using Theme Variables + +```css +/* Instead of hardcoded values */ +.button-old { + background: #007bff; + padding: 0.6em 1.2em; + border-radius: 4px; +} + +/* Use theme variables */ +.button-new { + background: var(--color-primary); + padding: var(--button-padding-y) var(--button-padding-x); + border-radius: var(--button-border-radius); +} +``` + +### Benefits + +1. **Consistency**: All components use the same design tokens +2. **Maintainability**: Change once in `theme.css`, updates everywhere +3. **Theme Switching**: Easy to implement dark mode (already scaffolded) +4. **Scalability**: Add new tokens without touching component styles +5. **Documentation**: Variable names are self-documenting + +### Utility Classes + +The theme file includes utility classes for common patterns: + +```html + +
Content
+ + +Important + + +
Items
+``` + +See `styles/THEME_USAGE_EXAMPLES.css` for complete examples of refactoring existing CSS to use theme variables. diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 225c2aa..c248c29 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,7 +7,7 @@ import GroceryList from "./pages/GroceryList.jsx"; import Login from "./pages/Login.jsx"; import Register from "./pages/Register.jsx"; -import AppLayout from "./components/AppLayout.jsx"; +import AppLayout from "./components/layout/AppLayout.jsx"; import PrivateRoute from "./utils/PrivateRoute.jsx"; import RoleGuard from "./utils/RoleGuard.jsx"; diff --git a/frontend/src/components/AddItemWithDetailsModal.jsx b/frontend/src/components/AddItemWithDetailsModal.jsx deleted file mode 100644 index dc42b30..0000000 --- a/frontend/src/components/AddItemWithDetailsModal.jsx +++ /dev/null @@ -1,186 +0,0 @@ -import { useRef, useState } from "react"; -import { ITEM_GROUPS, ITEM_TYPES, getItemTypeLabel, getZoneValues } from "../constants/classifications"; -import "../styles/AddItemWithDetailsModal.css"; - -export default function AddItemWithDetailsModal({ itemName, onConfirm, onSkip, onCancel }) { - const [selectedImage, setSelectedImage] = useState(null); - const [imagePreview, setImagePreview] = useState(null); - const [itemType, setItemType] = useState(""); - const [itemGroup, setItemGroup] = useState(""); - const [zone, setZone] = useState(""); - - const cameraInputRef = useRef(null); - const galleryInputRef = useRef(null); - - const handleImageChange = (e) => { - const file = e.target.files[0]; - if (file) { - setSelectedImage(file); - const reader = new FileReader(); - reader.onloadend = () => { - setImagePreview(reader.result); - }; - reader.readAsDataURL(file); - } - }; - - const removeImage = () => { - setSelectedImage(null); - setImagePreview(null); - }; - - const handleItemTypeChange = (e) => { - const newType = e.target.value; - setItemType(newType); - // Reset item group when type changes - setItemGroup(""); - }; - - const handleConfirm = () => { - // Validate classification if provided - if (itemType && !itemGroup) { - alert("Please select an item group"); - return; - } - - const classification = itemType ? { - item_type: itemType, - item_group: itemGroup, - zone: zone || null - } : null; - - onConfirm(selectedImage, classification); - }; - - const handleSkip = () => { - onSkip(); - }; - - const handleCameraClick = () => { - cameraInputRef.current?.click(); - }; - - const handleGalleryClick = () => { - galleryInputRef.current?.click(); - }; - - const availableGroups = itemType ? ITEM_GROUPS[itemType] || [] : []; - - return ( -
-
e.stopPropagation()}> -

Add Details for "{itemName}"

-

Add an image and classification to help organize your list

- - {/* Image Section */} -
-

Item Image (Optional)

-
- {!imagePreview ? ( -
- - -
- ) : ( -
- Preview - -
- )} -
- - - - -
- - {/* Classification Section */} -
-

Item Classification (Optional)

- -
- - -
- - {itemType && ( -
- - -
- )} - -
- - -
-
- - {/* Actions */} -
- - - -
-
-
- ); -} diff --git a/frontend/src/components/ErrorMessage.jsx b/frontend/src/components/common/ErrorMessage.jsx similarity index 100% rename from frontend/src/components/ErrorMessage.jsx rename to frontend/src/components/common/ErrorMessage.jsx diff --git a/frontend/src/components/FloatingActionButton.jsx b/frontend/src/components/common/FloatingActionButton.jsx similarity index 100% rename from frontend/src/components/FloatingActionButton.jsx rename to frontend/src/components/common/FloatingActionButton.jsx diff --git a/frontend/src/components/FormInput.jsx b/frontend/src/components/common/FormInput.jsx similarity index 100% rename from frontend/src/components/FormInput.jsx rename to frontend/src/components/common/FormInput.jsx diff --git a/frontend/src/components/SortDropdown.jsx b/frontend/src/components/common/SortDropdown.jsx similarity index 100% rename from frontend/src/components/SortDropdown.jsx rename to frontend/src/components/common/SortDropdown.jsx diff --git a/frontend/src/components/UserRoleCard.jsx b/frontend/src/components/common/UserRoleCard.jsx similarity index 92% rename from frontend/src/components/UserRoleCard.jsx rename to frontend/src/components/common/UserRoleCard.jsx index 0bb24dd..9a1405b 100644 --- a/frontend/src/components/UserRoleCard.jsx +++ b/frontend/src/components/common/UserRoleCard.jsx @@ -1,4 +1,4 @@ -import { ROLES } from "../constants/roles"; +import { ROLES } from "../../constants/roles"; export default function UserRoleCard({ user, onRoleChange }) { return ( diff --git a/frontend/src/components/common/index.js b/frontend/src/components/common/index.js new file mode 100644 index 0000000..0dc675e --- /dev/null +++ b/frontend/src/components/common/index.js @@ -0,0 +1,7 @@ +// Barrel export for common components +export { default as ErrorMessage } from './ErrorMessage.jsx'; +export { default as FloatingActionButton } from './FloatingActionButton.jsx'; +export { default as FormInput } from './FormInput.jsx'; +export { default as SortDropdown } from './SortDropdown.jsx'; +export { default as UserRoleCard } from './UserRoleCard.jsx'; + diff --git a/frontend/src/components/AddItemForm.jsx b/frontend/src/components/forms/AddItemForm.jsx similarity index 96% rename from frontend/src/components/AddItemForm.jsx rename to frontend/src/components/forms/AddItemForm.jsx index 10e76a7..cd26d02 100644 --- a/frontend/src/components/AddItemForm.jsx +++ b/frontend/src/components/forms/AddItemForm.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import SuggestionList from "./SuggestionList"; +import SuggestionList from "../items/SuggestionList"; export default function AddItemForm({ onAdd, onSuggest, suggestions, buttonText = "Add Item" }) { const [itemName, setItemName] = useState(""); diff --git a/frontend/src/components/forms/ClassificationSection.jsx b/frontend/src/components/forms/ClassificationSection.jsx new file mode 100644 index 0000000..f701dd2 --- /dev/null +++ b/frontend/src/components/forms/ClassificationSection.jsx @@ -0,0 +1,91 @@ +import { ITEM_GROUPS, ITEM_TYPES, getItemTypeLabel, getZoneValues } from "../../constants/classifications"; +import "../../styles/components/ClassificationSection.css"; + +/** + * Reusable classification component with cascading type/group/zone selects + * @param {Object} props + * @param {string} props.itemType - Selected item type + * @param {string} props.itemGroup - Selected item group + * @param {string} props.zone - Selected zone + * @param {Function} props.onItemTypeChange - Callback for type change (newType) + * @param {Function} props.onItemGroupChange - Callback for group change (newGroup) + * @param {Function} props.onZoneChange - Callback for zone change (newZone) + * @param {string} props.title - Section title (optional) + * @param {string} props.fieldClass - CSS class for field containers (optional) + * @param {string} props.selectClass - CSS class for select elements (optional) + */ +export default function ClassificationSection({ + itemType, + itemGroup, + zone, + onItemTypeChange, + onItemGroupChange, + onZoneChange, + title = "Item Classification (Optional)", + fieldClass = "classification-field", + selectClass = "classification-select" +}) { + const availableGroups = itemType ? ITEM_GROUPS[itemType] || [] : []; + + const handleTypeChange = (e) => { + const newType = e.target.value; + onItemTypeChange(newType); + // Parent should reset group when type changes + }; + + return ( +
+

{title}

+ +
+ + +
+ + {itemType && ( +
+ + +
+ )} + +
+ + +
+
+ ); +} diff --git a/frontend/src/components/forms/ImageUploadSection.jsx b/frontend/src/components/forms/ImageUploadSection.jsx new file mode 100644 index 0000000..1f57a09 --- /dev/null +++ b/frontend/src/components/forms/ImageUploadSection.jsx @@ -0,0 +1,77 @@ +import { useRef } from "react"; +import "../../styles/components/ImageUploadSection.css"; + +/** + * Reusable image upload component with camera and gallery options + * @param {Object} props + * @param {string} props.imagePreview - Base64 preview URL or null + * @param {Function} props.onImageChange - Callback when image is selected (file) + * @param {Function} props.onImageRemove - Callback to remove image + * @param {string} props.title - Section title (optional) + */ +export default function ImageUploadSection({ + imagePreview, + onImageChange, + onImageRemove, + title = "Item Image (Optional)" +}) { + const cameraInputRef = useRef(null); + const galleryInputRef = useRef(null); + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) { + onImageChange(file); + } + }; + + const handleCameraClick = () => { + cameraInputRef.current?.click(); + }; + + const handleGalleryClick = () => { + galleryInputRef.current?.click(); + }; + + return ( +
+

{title}

+
+ {!imagePreview ? ( +
+ + +
+ ) : ( +
+ Preview + +
+ )} +
+ + + + +
+ ); +} diff --git a/frontend/src/components/forms/index.js b/frontend/src/components/forms/index.js new file mode 100644 index 0000000..129ca8f --- /dev/null +++ b/frontend/src/components/forms/index.js @@ -0,0 +1,5 @@ +// Barrel export for form components +export { default as AddItemForm } from './AddItemForm.jsx'; +export { default as ClassificationSection } from './ClassificationSection.jsx'; +export { default as ImageUploadSection } from './ImageUploadSection.jsx'; + diff --git a/frontend/src/components/GroceryItem.tsx b/frontend/src/components/items/GroceryItem.tsx similarity index 95% rename from frontend/src/components/GroceryItem.tsx rename to frontend/src/components/items/GroceryItem.tsx index a352287..7daeba2 100644 --- a/frontend/src/components/GroceryItem.tsx +++ b/frontend/src/components/items/GroceryItem.tsx @@ -1,23 +1,23 @@ -import type { GroceryItemType } from "../types"; - -interface Props { - item: GroceryItemType; - onClick: (id: number) => void; -} - -export default function GroceryItem({ item, onClick }: Props) { - return ( -
  • onClick(item.id)} - style={{ - padding: "0.5em", - background: "#e9ecef", - marginBottom: "0.5em", - borderRadius: "4px", - cursor: "pointer", - }} - > - {item.item_name} ({item.quantity}) -
  • - ); -} +import type { GroceryItemType } from "../types"; + +interface Props { + item: GroceryItemType; + onClick: (id: number) => void; +} + +export default function GroceryItem({ item, onClick }: Props) { + return ( +
  • onClick(item.id)} + style={{ + padding: "0.5em", + background: "#e9ecef", + marginBottom: "0.5em", + borderRadius: "4px", + cursor: "pointer", + }} + > + {item.item_name} ({item.quantity}) +
  • + ); +} diff --git a/frontend/src/components/GroceryListItem.jsx b/frontend/src/components/items/GroceryListItem.jsx similarity index 96% rename from frontend/src/components/GroceryListItem.jsx rename to frontend/src/components/items/GroceryListItem.jsx index 6e8c02f..d2417cb 100644 --- a/frontend/src/components/GroceryListItem.jsx +++ b/frontend/src/components/items/GroceryListItem.jsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; -import AddImageModal from "./AddImageModal"; -import ConfirmBuyModal from "./ConfirmBuyModal"; -import ImageModal from "./ImageModal"; +import AddImageModal from "../modals/AddImageModal"; +import ConfirmBuyModal from "../modals/ConfirmBuyModal"; +import ImageModal from "../modals/ImageModal"; export default function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) { const [showModal, setShowModal] = useState(false); diff --git a/frontend/src/components/SuggestionList.tsx b/frontend/src/components/items/SuggestionList.tsx similarity index 95% rename from frontend/src/components/SuggestionList.tsx rename to frontend/src/components/items/SuggestionList.tsx index 51fd676..fc52eaa 100644 --- a/frontend/src/components/SuggestionList.tsx +++ b/frontend/src/components/items/SuggestionList.tsx @@ -1,40 +1,40 @@ -interface Props { - suggestions: string[]; - onSelect: (value: string) => void; -} - -export default function SuggestionList({ suggestions, onSelect }: Props) { - if (!suggestions.length) return null; - - return ( - - ); -} +interface Props { + suggestions: string[]; + onSelect: (value: string) => void; +} + +export default function SuggestionList({ suggestions, onSelect }: Props) { + if (!suggestions.length) return null; + + return ( + + ); +} diff --git a/frontend/src/components/items/index.js b/frontend/src/components/items/index.js new file mode 100644 index 0000000..f008559 --- /dev/null +++ b/frontend/src/components/items/index.js @@ -0,0 +1,5 @@ +// Barrel export for item-related components +export { default as GroceryItem } from './GroceryItem.tsx'; +export { default as GroceryListItem } from './GroceryListItem.jsx'; +export { default as SuggestionList } from './SuggestionList.tsx'; + diff --git a/frontend/src/components/AppLayout.jsx b/frontend/src/components/layout/AppLayout.jsx similarity index 94% rename from frontend/src/components/AppLayout.jsx rename to frontend/src/components/layout/AppLayout.jsx index b2d8b07..028174a 100644 --- a/frontend/src/components/AppLayout.jsx +++ b/frontend/src/components/layout/AppLayout.jsx @@ -1,11 +1,11 @@ -import { Outlet } from "react-router-dom"; -import Navbar from "./Navbar"; - -export default function AppLayout() { - return ( -
    - - -
    - ); -} +import { Outlet } from "react-router-dom"; +import Navbar from "./Navbar"; + +export default function AppLayout() { + return ( +
    + + +
    + ); +} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/layout/Navbar.jsx similarity index 85% rename from frontend/src/components/Navbar.jsx rename to frontend/src/components/layout/Navbar.jsx index c9d6331..b797d77 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/layout/Navbar.jsx @@ -1,30 +1,30 @@ -import "../styles/Navbar.css"; - -import { useContext } from "react"; -import { Link } from "react-router-dom"; -import { AuthContext } from "../context/AuthContext"; - -export default function Navbar() { - const { role, logout, username } = useContext(AuthContext); - - return ( - - ); +import "../../styles/components/Navbar.css"; + +import { useContext } from "react"; +import { Link } from "react-router-dom"; +import { AuthContext } from "../../context/AuthContext"; + +export default function Navbar() { + const { role, logout, username } = useContext(AuthContext); + + return ( + + ); } \ No newline at end of file diff --git a/frontend/src/components/layout/index.js b/frontend/src/components/layout/index.js new file mode 100644 index 0000000..1e2af60 --- /dev/null +++ b/frontend/src/components/layout/index.js @@ -0,0 +1,4 @@ +// Barrel export for layout components +export { default as AppLayout } from './AppLayout.jsx'; +export { default as Navbar } from './Navbar.jsx'; + diff --git a/frontend/src/components/AddImageModal.jsx b/frontend/src/components/modals/AddImageModal.jsx similarity index 98% rename from frontend/src/components/AddImageModal.jsx rename to frontend/src/components/modals/AddImageModal.jsx index 39ab266..a1ac428 100644 --- a/frontend/src/components/AddImageModal.jsx +++ b/frontend/src/components/modals/AddImageModal.jsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import "../styles/AddImageModal.css"; +import "../../styles/AddImageModal.css"; export default function AddImageModal({ itemName, onClose, onAddImage }) { const [selectedImage, setSelectedImage] = useState(null); diff --git a/frontend/src/components/modals/AddItemWithDetailsModal.jsx b/frontend/src/components/modals/AddItemWithDetailsModal.jsx new file mode 100644 index 0000000..b9e31fd --- /dev/null +++ b/frontend/src/components/modals/AddItemWithDetailsModal.jsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import "../../styles/components/AddItemWithDetailsModal.css"; +import ClassificationSection from "../forms/ClassificationSection"; +import ImageUploadSection from "../forms/ImageUploadSection"; + +export default function AddItemWithDetailsModal({ itemName, onConfirm, onSkip, onCancel }) { + const [selectedImage, setSelectedImage] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const [itemType, setItemType] = useState(""); + const [itemGroup, setItemGroup] = useState(""); + const [zone, setZone] = useState(""); + + const handleImageChange = (file) => { + setSelectedImage(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result); + }; + reader.readAsDataURL(file); + }; + + const handleImageRemove = () => { + setSelectedImage(null); + setImagePreview(null); + }; + + const handleItemTypeChange = (newType) => { + setItemType(newType); + setItemGroup(""); // Reset group when type changes + }; + + const handleConfirm = () => { + // Validate classification if provided + if (itemType && !itemGroup) { + alert("Please select an item group"); + return; + } + + const classification = itemType ? { + item_type: itemType, + item_group: itemGroup, + zone: zone || null + } : null; + + onConfirm(selectedImage, classification); + }; + + const handleSkip = () => { + onSkip(); + }; + + return ( +
    +
    e.stopPropagation()}> +

    Add Details for "{itemName}"

    +

    Add an image and classification to help organize your list

    + + {/* Image Section */} +
    + +
    + + {/* Classification Section */} +
    + +
    + + {/* Actions */} +
    + + + +
    +
    +
    + ); +} diff --git a/frontend/src/components/ConfirmBuyModal.jsx b/frontend/src/components/modals/ConfirmBuyModal.jsx similarity index 97% rename from frontend/src/components/ConfirmBuyModal.jsx rename to frontend/src/components/modals/ConfirmBuyModal.jsx index 3e9c7f6..e3a0dea 100644 --- a/frontend/src/components/ConfirmBuyModal.jsx +++ b/frontend/src/components/modals/ConfirmBuyModal.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import "../styles/ConfirmBuyModal.css"; +import "../../styles/ConfirmBuyModal.css"; export default function ConfirmBuyModal({ item, onConfirm, onCancel }) { const [quantity, setQuantity] = useState(item.quantity); diff --git a/frontend/src/components/EditItemModal.jsx b/frontend/src/components/modals/EditItemModal.jsx similarity index 59% rename from frontend/src/components/EditItemModal.jsx rename to frontend/src/components/modals/EditItemModal.jsx index 1f6c113..b7b5438 100644 --- a/frontend/src/components/EditItemModal.jsx +++ b/frontend/src/components/modals/EditItemModal.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { ITEM_GROUPS, ITEM_TYPES, getItemTypeLabel, getZoneValues } from "../constants/classifications"; -import "../styles/EditItemModal.css"; +import "../../styles/components/EditItemModal.css"; +import ClassificationSection from "../forms/ClassificationSection"; export default function EditItemModal({ item, onSave, onCancel }) { const [itemName, setItemName] = useState(item.item_name || ""); @@ -19,11 +19,9 @@ export default function EditItemModal({ item, onSave, onCancel }) { } }, [item]); - const handleItemTypeChange = (e) => { - const newType = e.target.value; + const handleItemTypeChange = (newType) => { setItemType(newType); - // Reset item group when type changes - setItemGroup(""); + setItemGroup(""); // Reset group when type changes }; const handleSave = async () => { @@ -60,8 +58,6 @@ export default function EditItemModal({ item, onSave, onCancel }) { } }; - const availableGroups = itemType ? ITEM_GROUPS[itemType] || [] : []; - return (
    e.stopPropagation()}> @@ -90,57 +86,16 @@ export default function EditItemModal({ item, onSave, onCancel }) {
    -

    Item Classification

    - -
    - - -
    - - {itemType && ( -
    - - -
    - )} - -
    - - -
    +
    +
    + )} )}
    diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 7768c49..7a100bc 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,10 +1,10 @@ import { useContext, useState } from "react"; import { Link } from "react-router-dom"; import { loginRequest } from "../api/auth"; -import ErrorMessage from "../components/ErrorMessage"; -import FormInput from "../components/FormInput"; +import ErrorMessage from "../components/common/ErrorMessage"; +import FormInput from "../components/common/FormInput"; import { AuthContext } from "../context/AuthContext"; -import "../styles/Login.css"; +import "../styles/pages/Login.css"; export default function Login() { const { login } = useContext(AuthContext); diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx index 6f42f9e..f39d20c 100644 --- a/frontend/src/pages/Register.jsx +++ b/frontend/src/pages/Register.jsx @@ -2,10 +2,10 @@ import { useContext, useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { loginRequest, registerRequest } from "../api/auth"; import { checkIfUserExists } from "../api/users"; -import ErrorMessage from "../components/ErrorMessage"; -import FormInput from "../components/FormInput"; +import ErrorMessage from "../components/common/ErrorMessage"; +import FormInput from "../components/common/FormInput"; import { AuthContext } from "../context/AuthContext"; -import "../styles/Register.css"; +import "../styles/pages/Register.css"; export default function Register() { const navigate = useNavigate(); diff --git a/frontend/src/styles/THEME_USAGE_EXAMPLES.css b/frontend/src/styles/THEME_USAGE_EXAMPLES.css new file mode 100644 index 0000000..16d24ea --- /dev/null +++ b/frontend/src/styles/THEME_USAGE_EXAMPLES.css @@ -0,0 +1,130 @@ +/** + * Theme Variable Usage Examples + * + * This file demonstrates how to refactor existing CSS to use theme variables. + * Copy these patterns when updating component styles. + */ + +/* ============================================ + BEFORE: Hardcoded values + ============================================ */ +.button-old { + background: #007bff; + color: white; + padding: 0.6em 1.2em; + border-radius: 4px; + border: none; + font-size: 1em; + transition: 0.2s; +} + +.button-old:hover { + background: #0056b3; +} + +/* ============================================ + AFTER: Using theme variables + ============================================ */ +.button-new { + background: var(--color-primary); + color: var(--color-text-inverse); + padding: var(--button-padding-y) var(--button-padding-x); + border-radius: var(--button-border-radius); + border: none; + font-size: var(--font-size-base); + font-weight: var(--button-font-weight); + transition: var(--transition-base); + cursor: pointer; +} + +.button-new:hover { + background: var(--color-primary-hover); +} + +/* ============================================ + MORE EXAMPLES + ============================================ */ + +/* Input Field */ +.input-field { + padding: var(--input-padding-y) var(--input-padding-x); + border: var(--border-width-thin) solid var(--input-border-color); + border-radius: var(--input-border-radius); + font-size: var(--font-size-base); + font-family: var(--font-family-base); + transition: var(--transition-base); +} + +.input-field:focus { + outline: none; + border-color: var(--input-focus-border-color); + box-shadow: var(--input-focus-shadow); +} + +/* Card Component */ +.card { + background: var(--card-bg); + padding: var(--card-padding); + border-radius: var(--card-border-radius); + box-shadow: var(--card-shadow); + margin-bottom: var(--spacing-md); +} + +/* Modal */ +.modal-overlay { + background: var(--modal-backdrop-bg); + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-modal); +} + +.modal-content { + background: var(--modal-bg); + padding: var(--modal-padding); + border-radius: var(--modal-border-radius); + max-width: var(--modal-max-width); + box-shadow: var(--shadow-xl); +} + +/* Text Styles */ +.heading-primary { + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); +} + +.text-muted { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} + +/* Spacing Examples */ +.section { + margin-bottom: var(--spacing-xl); +} + +.field-group { + margin-bottom: var(--spacing-md); +} + +/* Border Examples */ +.divider { + border-bottom: var(--border-width-thin) solid var(--color-border-light); + margin: var(--spacing-lg) 0; +} + +/* ============================================ + BENEFITS OF USING THEME VARIABLES + ============================================ + + 1. Consistency: All components use the same colors/spacing + 2. Maintainability: Change once, update everywhere + 3. Theme switching: Easy to implement dark mode + 4. Scalability: Add new colors/sizes without touching components + 5. Documentation: Variable names are self-documenting + +*/ diff --git a/frontend/src/styles/AddItemWithDetailsModal.css b/frontend/src/styles/components/AddItemWithDetailsModal.css similarity index 100% rename from frontend/src/styles/AddItemWithDetailsModal.css rename to frontend/src/styles/components/AddItemWithDetailsModal.css diff --git a/frontend/src/styles/components/ClassificationSection.css b/frontend/src/styles/components/ClassificationSection.css new file mode 100644 index 0000000..1b06669 --- /dev/null +++ b/frontend/src/styles/components/ClassificationSection.css @@ -0,0 +1,44 @@ +/* Classification Section */ +.classification-section { + margin-bottom: 1.5rem; +} + +.classification-title { + font-size: 1em; + font-weight: 600; + margin-bottom: 0.8rem; + color: #333; +} + +.classification-field { + margin-bottom: 1rem; +} + +.classification-field label { + display: block; + font-size: 0.9em; + font-weight: 500; + margin-bottom: 0.4rem; + color: #555; +} + +.classification-select { + width: 100%; + padding: 0.6rem; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + transition: border-color 0.2s; +} + +.classification-select:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1); +} + +.classification-select:hover { + border-color: #999; +} diff --git a/frontend/src/styles/EditItemModal.css b/frontend/src/styles/components/EditItemModal.css similarity index 100% rename from frontend/src/styles/EditItemModal.css rename to frontend/src/styles/components/EditItemModal.css diff --git a/frontend/src/styles/components/ImageUploadSection.css b/frontend/src/styles/components/ImageUploadSection.css new file mode 100644 index 0000000..79c1378 --- /dev/null +++ b/frontend/src/styles/components/ImageUploadSection.css @@ -0,0 +1,86 @@ +/* Image Upload Section */ +.image-upload-section { + margin-bottom: 1.5rem; +} + +.image-upload-title { + font-size: 1em; + font-weight: 600; + margin-bottom: 0.8rem; + color: #333; +} + +.image-upload-content { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 1rem; + background: #f9f9f9; +} + +.image-upload-options { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.image-upload-btn { + padding: 0.8rem 1rem; + font-size: 1em; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + font-weight: 500; +} + +.image-upload-btn.camera { + background: #007bff; + color: white; +} + +.image-upload-btn.camera:hover { + background: #0056b3; +} + +.image-upload-btn.gallery { + background: #6c757d; + color: white; +} + +.image-upload-btn.gallery:hover { + background: #545b62; +} + +.image-upload-preview { + position: relative; + max-width: 300px; + margin: 0 auto; +} + +.image-upload-preview img { + width: 100%; + border-radius: 8px; + display: block; +} + +.image-upload-remove { + position: absolute; + top: 8px; + right: 8px; + background: rgba(255, 0, 0, 0.8); + color: white; + border: none; + border-radius: 50%; + width: 30px; + height: 30px; + font-size: 1.2em; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} + +.image-upload-remove:hover { + background: rgba(255, 0, 0, 1); +} diff --git a/frontend/src/styles/Navbar.css b/frontend/src/styles/components/Navbar.css similarity index 93% rename from frontend/src/styles/Navbar.css rename to frontend/src/styles/components/Navbar.css index 5bca7aa..30d23a2 100644 --- a/frontend/src/styles/Navbar.css +++ b/frontend/src/styles/components/Navbar.css @@ -1,58 +1,58 @@ -.navbar { - background: #343a40; - color: white; - padding: 0.6em 1em; - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 4px; - margin-bottom: 1em; -} - -.navbar-links a { - color: white; - margin-right: 1em; - text-decoration: none; - font-size: 1.1em; -} - -.navbar-links a:hover { - text-decoration: underline; -} - -.navbar-logout { - background: #dc3545; - color: white; - border: none; - padding: 0.4em 0.8em; - border-radius: 4px; - cursor: pointer; - width: 100px; -} - -.navbar-idcard { - display: flex; - align-items: center; - align-content: center; - margin-right: 1em; - padding: 0.3em 0.6em; - background: #495057; - border-radius: 4px; - color: white; -} - -.navbar-idinfo { - display: flex; - flex-direction: column; - line-height: 1.1; -} - -.navbar-username { - font-size: 0.95em; - font-weight: bold; -} - -.navbar-role { - font-size: 0.75em; - opacity: 0.8; -} +.navbar { + background: #343a40; + color: white; + padding: 0.6em 1em; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 4px; + margin-bottom: 1em; +} + +.navbar-links a { + color: white; + margin-right: 1em; + text-decoration: none; + font-size: 1.1em; +} + +.navbar-links a:hover { + text-decoration: underline; +} + +.navbar-logout { + background: #dc3545; + color: white; + border: none; + padding: 0.4em 0.8em; + border-radius: 4px; + cursor: pointer; + width: 100px; +} + +.navbar-idcard { + display: flex; + align-items: center; + align-content: center; + margin-right: 1em; + padding: 0.3em 0.6em; + background: #495057; + border-radius: 4px; + color: white; +} + +.navbar-idinfo { + display: flex; + flex-direction: column; + line-height: 1.1; +} + +.navbar-username { + font-size: 0.95em; + font-weight: bold; +} + +.navbar-role { + font-size: 0.75em; + opacity: 0.8; +} diff --git a/frontend/src/styles/GroceryList.css b/frontend/src/styles/pages/GroceryList.css similarity index 69% rename from frontend/src/styles/GroceryList.css rename to frontend/src/styles/pages/GroceryList.css index b07e1f6..e50c865 100644 --- a/frontend/src/styles/GroceryList.css +++ b/frontend/src/styles/pages/GroceryList.css @@ -1,304 +1,323 @@ -/* Container */ -.glist-body { - font-family: Arial, sans-serif; - padding: 1em; - background: #f8f9fa; -} - -.glist-container { - max-width: 480px; - margin: auto; - background: white; - padding: 1em; - border-radius: 8px; - box-shadow: 0 0 10px rgba(0,0,0,0.08); -} - -/* Title */ -.glist-title { - text-align: center; - font-size: 1.5em; - margin-bottom: 0.4em; -} - -.glist-section-title { - text-align: center; - font-size: 1.2em; - margin-top: 2em; - margin-bottom: 0.5em; - color: #495057; - border-top: 2px solid #e0e0e0; - padding-top: 1em; -} - -/* Classification Groups */ -.glist-classification-group { - margin-bottom: 2em; -} - -.glist-classification-header { - font-size: 1.1em; - font-weight: 600; - color: #007bff; - margin: 1em 0 0.5em 0; - padding: 0.5em 0.8em; - background: #e7f3ff; - border-left: 4px solid #007bff; - border-radius: 4px; -} - -/* Inputs */ -.glist-input { - font-size: 1em; - padding: 0.5em; - margin: 0.3em 0; - width: 100%; - box-sizing: border-box; -} - -/* Buttons */ -.glist-btn { - font-size: 1em; - padding: 0.55em; - width: 100%; - margin-top: 0.4em; - cursor: pointer; - border: none; - background: #007bff; - color: white; - border-radius: 4px; -} - -.glist-btn:hover { - background: #0067d8; -} - -/* Suggestion dropdown */ -.glist-suggest-box { - background: #fff; - border: 1px solid #ccc; - max-height: 150px; - overflow-y: auto; - position: absolute; - z-index: 999; - border-radius: 8px; - box-shadow: 0 0 10px rgba(0,0,0,0.08); - padding: 1em; - width: calc(100% - 8em); - max-width: 440px; - margin: 0 auto; -} - -.glist-suggest-item { - padding: 0.5em; - padding-inline: 2em; - cursor: pointer; -} - -.glist-suggest-item:hover { - background: #eee; -} - -/* Grocery list items */ -.glist-ul { - list-style: none; - padding: 0; - margin-top: 1em; -} - -.glist-li { - background: #fff; - border: 1px solid #e0e0e0; - border-radius: 8px; - margin-bottom: 0.8em; - cursor: pointer; - transition: box-shadow 0.2s, transform 0.2s; - overflow: hidden; -} - -.glist-li:hover { - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - transform: translateY(-2px); -} - -.glist-item-layout { - display: flex; - gap: 1em; - padding: 0em; - align-items: center; -} - -.glist-item-image { - width: 50px; - height: 50px; - min-width: 50px; - background: #f5f5f5; - border: 2px solid #e0e0e0; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - font-size: 2em; - color: #ccc; - overflow: hidden; - position: relative; -} - -.glist-item-image.has-image { - border-color: #007bff; - background: #fff; -} - -.glist-item-image img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.glist-item-image.has-image:hover { - opacity: 0.8; - box-shadow: 0 0 8px rgba(0, 123, 255, 0.3); -} - -.glist-item-content { - display: flex; - flex-direction: column; - gap: 0.4em; - flex: 1; - min-width: 0; -} - -.glist-item-header { - display: flex; - align-items: baseline; - gap: 0.5em; - flex-wrap: wrap; -} - -.glist-item-name { - font-weight: 800; - font-size: 0.8em; - color: #333; -} - -.glist-item-quantity { - position: absolute; - top: 0; - right: 0; - background: rgba(0, 123, 255, 0.9); - color: white; - font-weight: 700; - font-size: 0.3em; - padding: 0.2em 0.4em; - border-radius: 0 6px 0 4px; - min-width: 20%; - text-align: center; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); -} - -.glist-item-users { - font-size: 0.7em; - color: #888; - font-style: italic; -} - -/* Sorting dropdown */ -.glist-sort { - width: 100%; - margin: 0.3em 0; - padding: 0.5em; - font-size: 1em; - border-radius: 4px; -} - -/* Image upload */ -.glist-image-upload { - margin: 0.5em 0; -} - -.glist-image-label { - display: block; - padding: 0.6em; - background: #f0f0f0; - border: 2px dashed #ccc; - border-radius: 4px; - text-align: center; - cursor: pointer; - transition: all 0.2s; -} - -.glist-image-label:hover { - background: #e8e8e8; - border-color: #007bff; -} - -.glist-image-preview { - position: relative; - margin-top: 0.5em; - display: inline-block; -} - -.glist-image-preview img { - max-width: 150px; - max-height: 150px; - border-radius: 8px; - border: 2px solid #ddd; -} - -.glist-remove-image { - position: absolute; - top: -8px; - right: -8px; - width: 28px; - height: 28px; - border-radius: 50%; - background: #ff4444; - color: white; - border: 2px solid white; - font-size: 1.2rem; - line-height: 1; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} - -.glist-remove-image:hover { - background: #cc0000; -} - -/* Floating Action Button (FAB) */ -.glist-fab { - position: fixed; - bottom: 20px; - right: 20px; - background: #28a745; - color: white; - border: none; - border-radius: 50%; - width: 62px; - height: 62px; - font-size: 2em; - line-height: 0; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 3px 10px rgba(0,0,0,0.2); - cursor: pointer; -} - -.glist-fab:hover { - background: #218838; -} - -/* Mobile tweaks */ -@media (max-width: 480px) { - .glist-container { - padding: 1em 0.8em; - } - - .glist-fab { - bottom: 16px; - right: 16px; - } -} +/* Container */ +.glist-body { + font-family: var(--font-family-base); + padding: var(--spacing-md); + background: var(--color-bg-body); +} + +.glist-container { + max-width: var(--container-max-width); + margin: auto; + background: var(--color-bg-surface); + padding: var(--spacing-md); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-card); +} + +/* Title */ +.glist-title { + text-align: center; + font-size: var(--font-size-2xl); + margin-bottom: var(--spacing-sm); +} + +.glist-section-title { + text-align: center; + font-size: var(--font-size-xl); + margin-top: var(--spacing-xl); + margin-bottom: var(--spacing-sm); + color: var(--color-gray-700); + border-top: var(--border-width-medium) solid var(--color-border-light); + padding-top: var(--spacing-md); +} + +/* Classification Groups */ +.glist-classification-group { + margin-bottom: var(--spacing-xl); +} + +.glist-classification-header { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-primary); + margin: var(--spacing-md) 0 var(--spacing-sm) 0; + padding: var(--spacing-sm) var(--spacing-md); + background: var(--color-primary-light); + border-left: var(--border-width-thick) solid var(--color-primary); + border-radius: var(--border-radius-sm); +} + +/* Inputs */ +.glist-input { + font-size: 1em; + padding: 0.5em; + margin: 0.3em 0; + width: 100%; + box-sizing: border-box; +} + +/* Buttons */ +.glist-btn { + font-size: var(--font-size-base); + padding: var(--button-padding-y); + width: 100%; + margin-top: var(--spacing-sm); + cursor: pointer; + border: none; + background: var(--color-primary); + color: var(--color-text-inverse); + border-radius: var(--button-border-radius); + font-weight: var(--button-font-weight); + transition: var(--transition-base); +} + +.glist-btn:hover { + background: var(--color-primary-dark); +} + +.glist-show-more-btn { + font-size: var(--font-size-sm); + padding: var(--spacing-sm) var(--spacing-lg); + cursor: pointer; + border: var(--border-width-thin) solid var(--color-primary); + background: var(--color-bg-surface); + color: var(--color-primary); + border-radius: var(--button-border-radius); + transition: var(--transition-base); + font-weight: var(--button-font-weight); +} + +.glist-show-more-btn:hover { + background: var(--color-primary); + color: var(--color-text-inverse); +} + +/* Suggestion dropdown */ +.glist-suggest-box { + background: #fff; + border: 1px solid #ccc; + max-height: 150px; + overflow-y: auto; + position: absolute; + z-index: 999; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0,0,0,0.08); + padding: 1em; + width: calc(100% - 8em); + max-width: 440px; + margin: 0 auto; +} + +.glist-suggest-item { + padding: 0.5em; + padding-inline: 2em; + cursor: pointer; +} + +.glist-suggest-item:hover { + background: #eee; +} + +/* Grocery list items */ +.glist-ul { + list-style: none; + padding: 0; + margin-top: 1em; +} + +.glist-li { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; + margin-bottom: 0.8em; + cursor: pointer; + transition: box-shadow 0.2s, transform 0.2s; + overflow: hidden; +} + +.glist-li:hover { + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transform: translateY(-2px); +} + +.glist-item-layout { + display: flex; + gap: 1em; + padding: 0em; + align-items: center; +} + +.glist-item-image { + width: 50px; + height: 50px; + min-width: 50px; + background: #f5f5f5; + border: 2px solid #e0e0e0; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 2em; + color: #ccc; + overflow: hidden; + position: relative; +} + +.glist-item-image.has-image { + border-color: #007bff; + background: #fff; +} + +.glist-item-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.glist-item-image.has-image:hover { + opacity: 0.8; + box-shadow: 0 0 8px rgba(0, 123, 255, 0.3); +} + +.glist-item-content { + display: flex; + flex-direction: column; + gap: 0.4em; + flex: 1; + min-width: 0; +} + +.glist-item-header { + display: flex; + align-items: baseline; + gap: 0.5em; + flex-wrap: wrap; +} + +.glist-item-name { + font-weight: 800; + font-size: 0.8em; + color: #333; +} + +.glist-item-quantity { + position: absolute; + top: 0; + right: 0; + background: rgba(0, 123, 255, 0.9); + color: white; + font-weight: 700; + font-size: 0.3em; + padding: 0.2em 0.4em; + border-radius: 0 6px 0 4px; + min-width: 20%; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.glist-item-users { + font-size: 0.7em; + color: #888; + font-style: italic; +} + +/* Sorting dropdown */ +.glist-sort { + width: 100%; + margin: 0.3em 0; + padding: 0.5em; + font-size: 1em; + border-radius: 4px; +} + +/* Image upload */ +.glist-image-upload { + margin: 0.5em 0; +} + +.glist-image-label { + display: block; + padding: 0.6em; + background: #f0f0f0; + border: 2px dashed #ccc; + border-radius: 4px; + text-align: center; + cursor: pointer; + transition: all 0.2s; +} + +.glist-image-label:hover { + background: #e8e8e8; + border-color: #007bff; +} + +.glist-image-preview { + position: relative; + margin-top: 0.5em; + display: inline-block; +} + +.glist-image-preview img { + max-width: 150px; + max-height: 150px; + border-radius: 8px; + border: 2px solid #ddd; +} + +.glist-remove-image { + position: absolute; + top: -8px; + right: -8px; + width: 28px; + height: 28px; + border-radius: 50%; + background: #ff4444; + color: white; + border: 2px solid white; + font-size: 1.2rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.glist-remove-image:hover { + background: #cc0000; +} + +/* Floating Action Button (FAB) */ +.glist-fab { + position: fixed; + bottom: 20px; + right: 20px; + background: #28a745; + color: white; + border: none; + border-radius: 50%; + width: 62px; + height: 62px; + font-size: 2em; + line-height: 0; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 3px 10px rgba(0,0,0,0.2); + cursor: pointer; +} + +.glist-fab:hover { + background: #218838; +} + +/* Mobile tweaks */ +@media (max-width: 480px) { + .glist-container { + padding: 1em 0.8em; + } + + .glist-fab { + bottom: 16px; + right: 16px; + } +} diff --git a/frontend/src/styles/Login.css b/frontend/src/styles/pages/Login.css similarity index 100% rename from frontend/src/styles/Login.css rename to frontend/src/styles/pages/Login.css diff --git a/frontend/src/styles/Register.css b/frontend/src/styles/pages/Register.css similarity index 100% rename from frontend/src/styles/Register.css rename to frontend/src/styles/pages/Register.css diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css new file mode 100644 index 0000000..a8d4f5c --- /dev/null +++ b/frontend/src/styles/theme.css @@ -0,0 +1,268 @@ +/** + * Global Theme Variables + * + * This file defines the design system for the entire application. + * All colors, spacing, typography, and other design tokens are centralized here. + * + * Usage: var(--variable-name) + * Example: color: var(--color-primary); + */ + +:root { + /* ============================================ + COLOR PALETTE + ============================================ */ + + /* Primary Colors */ + --color-primary: #007bff; + --color-primary-hover: #0056b3; + --color-primary-light: #e7f3ff; + --color-primary-dark: #0067d8; + + /* Secondary Colors */ + --color-secondary: #6c757d; + --color-secondary-hover: #545b62; + --color-secondary-light: #f8f9fa; + + /* Semantic Colors */ + --color-success: #28a745; + --color-success-hover: #218838; + --color-success-light: #d4edda; + + --color-danger: #dc3545; + --color-danger-hover: #c82333; + --color-danger-light: #f8d7da; + + --color-warning: #ffc107; + --color-warning-hover: #e0a800; + --color-warning-light: #fff3cd; + + --color-info: #17a2b8; + --color-info-hover: #138496; + --color-info-light: #d1ecf1; + + /* Neutral Colors */ + --color-white: #ffffff; + --color-black: #000000; + --color-gray-50: #f9f9f9; + --color-gray-100: #f8f9fa; + --color-gray-200: #e9ecef; + --color-gray-300: #dee2e6; + --color-gray-400: #ced4da; + --color-gray-500: #adb5bd; + --color-gray-600: #6c757d; + --color-gray-700: #495057; + --color-gray-800: #343a40; + --color-gray-900: #212529; + + /* Text Colors */ + --color-text-primary: #212529; + --color-text-secondary: #6c757d; + --color-text-muted: #adb5bd; + --color-text-inverse: #ffffff; + + /* Background Colors */ + --color-bg-body: #f8f9fa; + --color-bg-surface: #ffffff; + --color-bg-hover: #f5f5f5; + --color-bg-disabled: #e9ecef; + + /* Border Colors */ + --color-border-light: #e0e0e0; + --color-border-medium: #ccc; + --color-border-dark: #999; + + /* ============================================ + SPACING + ============================================ */ + --spacing-xs: 0.25rem; /* 4px */ + --spacing-sm: 0.5rem; /* 8px */ + --spacing-md: 1rem; /* 16px */ + --spacing-lg: 1.5rem; /* 24px */ + --spacing-xl: 2rem; /* 32px */ + --spacing-2xl: 3rem; /* 48px */ + --spacing-3xl: 4rem; /* 64px */ + + /* ============================================ + TYPOGRAPHY + ============================================ */ + --font-family-base: Arial, sans-serif; + --font-family-heading: Arial, sans-serif; + --font-family-mono: 'Courier New', monospace; + + /* Font Sizes */ + --font-size-xs: 0.75rem; /* 12px */ + --font-size-sm: 0.875rem; /* 14px */ + --font-size-base: 1rem; /* 16px */ + --font-size-lg: 1.125rem; /* 18px */ + --font-size-xl: 1.25rem; /* 20px */ + --font-size-2xl: 1.5rem; /* 24px */ + --font-size-3xl: 2rem; /* 32px */ + + /* Font Weights */ + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* Line Heights */ + --line-height-tight: 1.2; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; + + /* ============================================ + BORDERS & RADIUS + ============================================ */ + --border-width-thin: 1px; + --border-width-medium: 2px; + --border-width-thick: 4px; + + --border-radius-sm: 4px; + --border-radius-md: 6px; + --border-radius-lg: 8px; + --border-radius-xl: 12px; + --border-radius-full: 50%; + + /* ============================================ + SHADOWS + ============================================ */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + --shadow-card: 0 0 10px rgba(0, 0, 0, 0.08); + + /* ============================================ + TRANSITIONS + ============================================ */ + --transition-fast: 0.15s ease; + --transition-base: 0.2s ease; + --transition-slow: 0.3s ease; + + /* ============================================ + Z-INDEX LAYERS + ============================================ */ + --z-dropdown: 100; + --z-sticky: 200; + --z-fixed: 300; + --z-modal-backdrop: 900; + --z-modal: 1000; + --z-tooltip: 1100; + + /* ============================================ + LAYOUT + ============================================ */ + --container-max-width: 480px; + --container-padding: var(--spacing-md); + + /* ============================================ + COMPONENT-SPECIFIC + ============================================ */ + + /* Buttons */ + --button-padding-y: 0.6rem; + --button-padding-x: 1.5rem; + --button-border-radius: var(--border-radius-sm); + --button-font-weight: var(--font-weight-medium); + + /* Inputs */ + --input-padding-y: 0.6rem; + --input-padding-x: 0.75rem; + --input-border-color: var(--color-border-medium); + --input-border-radius: var(--border-radius-sm); + --input-focus-border-color: var(--color-primary); + --input-focus-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1); + + /* Cards */ + --card-bg: var(--color-bg-surface); + --card-padding: var(--spacing-md); + --card-border-radius: var(--border-radius-lg); + --card-shadow: var(--shadow-card); + + /* Modals */ + --modal-backdrop-bg: rgba(0, 0, 0, 0.5); + --modal-bg: var(--color-white); + --modal-border-radius: var(--border-radius-lg); + --modal-padding: var(--spacing-lg); + --modal-max-width: 500px; +} + +/* ============================================ + DARK MODE SUPPORT (Future Implementation) + ============================================ */ +@media (prefers-color-scheme: dark) { + /* Uncomment to enable dark mode + :root { + --color-text-primary: #f8f9fa; + --color-text-secondary: #adb5bd; + --color-bg-body: #212529; + --color-bg-surface: #343a40; + --color-border-light: #495057; + --color-border-medium: #6c757d; + } + */ +} + +/* Manual dark mode class override */ +.dark-mode { + --color-text-primary: #f8f9fa; + --color-text-secondary: #adb5bd; + --color-bg-body: #212529; + --color-bg-surface: #343a40; + --color-border-light: #495057; + --color-border-medium: #6c757d; +} + +/* ============================================ + UTILITY CLASSES + ============================================ */ + +/* Spacing Utilities */ +.m-0 { margin: 0 !important; } +.mt-1 { margin-top: var(--spacing-xs) !important; } +.mt-2 { margin-top: var(--spacing-sm) !important; } +.mt-3 { margin-top: var(--spacing-md) !important; } +.mt-4 { margin-top: var(--spacing-lg) !important; } + +.mb-1 { margin-bottom: var(--spacing-xs) !important; } +.mb-2 { margin-bottom: var(--spacing-sm) !important; } +.mb-3 { margin-bottom: var(--spacing-md) !important; } +.mb-4 { margin-bottom: var(--spacing-lg) !important; } + +.p-0 { padding: 0 !important; } +.p-1 { padding: var(--spacing-xs) !important; } +.p-2 { padding: var(--spacing-sm) !important; } +.p-3 { padding: var(--spacing-md) !important; } +.p-4 { padding: var(--spacing-lg) !important; } + +/* Text Utilities */ +.text-center { text-align: center !important; } +.text-left { text-align: left !important; } +.text-right { text-align: right !important; } + +.text-primary { color: var(--color-primary) !important; } +.text-secondary { color: var(--color-text-secondary) !important; } +.text-muted { color: var(--color-text-muted) !important; } +.text-danger { color: var(--color-danger) !important; } +.text-success { color: var(--color-success) !important; } + +.font-weight-normal { font-weight: var(--font-weight-normal) !important; } +.font-weight-medium { font-weight: var(--font-weight-medium) !important; } +.font-weight-semibold { font-weight: var(--font-weight-semibold) !important; } +.font-weight-bold { font-weight: var(--font-weight-bold) !important; } + +/* Display Utilities */ +.d-none { display: none !important; } +.d-block { display: block !important; } +.d-flex { display: flex !important; } +.d-inline-block { display: inline-block !important; } + +/* Flex Utilities */ +.flex-column { flex-direction: column !important; } +.flex-row { flex-direction: row !important; } +.justify-center { justify-content: center !important; } +.justify-between { justify-content: space-between !important; } +.align-center { align-items: center !important; } +.gap-1 { gap: var(--spacing-xs) !important; } +.gap-2 { gap: var(--spacing-sm) !important; } +.gap-3 { gap: var(--spacing-md) !important; }