kanban-app/docs/usage_rules_frontend.md
2026-02-27 17:03:47 +03:00

19 KiB

Frontend Development Rules for AI/LLM

This document provides guidelines and rules that AI/LLM assistants should follow when writing or modifying frontend code for the Crafting Shop application.

Architecture Principles

State Management

  • ALWAYS use React Context API for global state management
  • NEVER use Zustand, Redux, or other state management libraries
  • Store context in frontend/src/context/
  • Use useContext() hook for accessing context
// ✅ CORRECT
import { useContext } from "react"
import { AppContext } from "../context/AppContext"

function Component() {
  const { user, setUser } = useContext(AppContext)
  // ...
}

// ❌ WRONG
import { create } from "zustand"

API Calls

  • ALWAYS use the custom useApi hook for all API calls
  • NEVER use axios directly in components
  • Import useApi from frontend/src/hooks/useApi.js
// ✅ CORRECT
import useApi from "../hooks/useApi"

function Products() {
  const { api } = useApi()
  const [products, setProducts] = useState([])
  
  useEffect(() => {
    api.get("/api/products")
      .then(response => setProducts(response.data))
      .catch(error => console.error(error))
  }, [api])
}

// ❌ WRONG
import axios from "axios"

function Products() {
  useEffect(() => {
    axios.get("/api/products") // Don't do this!
  }, [])
}

API Integration with Loaders and Toasts

  • ALWAYS create custom hooks for API operations that integrate loader and toast logic
  • NEVER handle errors in components when the hook already shows toasts
  • Custom hooks should use useLoader and useToast for consistent UX
  • Hooks should return data, refetch function, and optionally error state for debugging
  • Components should NOT display error UI when toasts are already shown
// ✅ CORRECT - Custom hook with loader and toast integration
import { useState, useEffect } from "react"
import useApi from "./useApi"
import { useLoader } from "../context/loaders/useLoader"
import { useToast } from "../context/toasts/useToast"

function useProducts() {
  const [products, setProducts] = useState([])
  const [error, setError] = useState(null)
  
  const { getProducts } = useApi()
  const { withLoader } = useLoader()
  const { addNotification } = useToast()
  
  const fetchProducts = async () => {
    try {
      setError(null)
      
      // Use withLoader to show loading state
      const data = await withLoader(
        () => getProducts(),
        'Loading products...'
      )
      
      setProducts(data)
      
      // Show success toast
      addNotification({
        type: 'success',
        title: 'Products Loaded',
        message: `Successfully loaded ${data.length} products.`,
        duration: 3000,
      })
      
      return data
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Failed to load products'
      setError(errorMessage)
      
      // Show error toast
      addNotification({
        type: 'error',
        title: 'Error Loading Products',
        message: errorMessage,
        duration: 5000,
      })
      
      return []
    }
  }
  
  useEffect(() => {
    fetchProducts()
  }, [])
  
  return {
    products,
    error, // For debugging, not for UI display
    loading: false, // Loading is handled by global loader
    refetch: fetchProducts,
  }
}

export default useProducts
// ✅ CORRECT - Component using the custom hook
function Products() {
  const { products, refetch } = useProducts()
  
  // Note: No error UI here - toast is already shown by the hook
  
  return (
    <div>
      <h1>Products</h1>
      <button onClick={() => refetch()}>Refresh</button>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}
// ❌ WRONG - Handling errors in component when toast already shown
function Products() {
  const { products, error, refetch } = useProducts()
  
  // BAD PRACTICE: This duplicates the error message already shown in toast
  if (error) {
    return (
      <div className="text-center py-12">
        <p className="text-red-400 mb-4">Failed to load products</p>
        <button onClick={() => refetch()}>Retry</button>
      </div>
    )
  }
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

Why this is bad practice:

  1. Duplicate UI: Users see the same error message twice (toast + component error UI)
  2. Inconsistent UX: Some operations show toasts, others show component error messages
  3. Confusing Experience: Users don't know which error message to act on
  4. Redundant Code: You're writing error handling twice (in hook and component)
  5. Maintenance Issue: If you change error handling in hook, component UI doesn't update

The error state in hooks is for:

  • Debugging purposes
  • Conditional rendering based on error type (rare cases)
  • Logging or analytics
  • NOT for displaying error UI to users

Benefits of this pattern:

  1. Consistent UX: All errors shown via toasts in the same place (top-right)
  2. Separation of Concerns: Hook handles data fetching, component handles rendering
  3. Global Loading: Loader covers entire app, preventing double-submissions
  4. Auto-Cleanup: withLoader ensures loader is hidden even on errors
  5. Clean Components: Components focus on displaying data, not handling errors

## Code Style

### Formatting
- Use double quotes for strings and object properties
- Use functional components with hooks (no class components)
- Use arrow functions for callbacks
- Follow React best practices
- Maximum line length: 100 characters

### Naming Conventions
- Components: PascalCase (e.g., `ProductCard`, `Navbar`)
- Hooks: camelCase with `use` prefix (e.g., `useProducts`, `useAuth`)
- Files: kebab-case (e.g., `product-card.jsx`, `navbar.jsx`)
- Directories: lowercase (e.g., `components/`, `pages/`)

## Component Rules

### Functional Components
- **ALWAYS** use functional components with React hooks
- **NEVER** use class components
- Destructure props at the top of component

```jsx
// ✅ CORRECT
import React from "react"

function ProductCard({ name, price, onClick }) {
  return (
    <div className="product-card" onClick={onClick}>
      <h3>{name}</h3>
      <p>${price}</p>
    </div>
  )
}

export default ProductCard

Props Validation

  • Use PropTypes for component props
  • Define PropTypes at the bottom of file
  • Mark required props
import PropTypes from "prop-types"

function ProductCard({ name, price, onClick }) {
  // ...
}

ProductCard.propTypes = {
  name: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  onClick: PropTypes.func
}

Conditional Rendering

  • Use ternary operators for simple conditions
  • Use logical AND for conditional elements
  • Keep JSX readable
// ✅ CORRECT
{isLoading ? <Loader /> : <Content />}
{error && <ErrorMessage message={error} />}
{user && <WelcomeMessage user={user} />}

Lists Rendering

  • ALWAYS provide unique key prop
  • Use .map() for transforming arrays to elements
// ✅ CORRECT
{products.map(product => (
  <ProductCard 
    key={product.id} 
    product={product} 
  />
))}

Hook Rules

Custom Hooks

  • Create custom hooks for reusable logic
  • Name hooks with use prefix
  • Place hooks in frontend/src/hooks/
// ✅ CORRECT - Create custom hook
import { useState, useEffect } from "react"
import useApi from "./useApi"

function useProducts() {
  const { api } = useApi()
  const [products, setProducts] = useState([])
  const [loading, setLoading] = useState(false)
  
  useEffect(() => {
    setLoading(true)
    api.get("/api/products")
      .then(response => setProducts(response.data))
      .finally(() => setLoading(false))
  }, [api])
  
  return { products, loading }
}

export default useProducts

Effect Rules

  • ALWAYS include all dependencies in dependency array
  • Use proper cleanup in useEffect
  • Avoid infinite loops
// ✅ CORRECT
useEffect(() => {
  const fetchData = async () => {
    const response = await api.get("/api/products")
    setData(response.data)
  }
  
  fetchData()
  
  return () => {
    // Cleanup
  }
}, [api]) // Include all dependencies

Styling Rules

Tailwind CSS

  • ALWAYS use Tailwind CSS for styling
  • Use utility classes for styling
  • Avoid custom CSS files when possible
  • Use className prop (not class)
// ✅ CORRECT
<div className="bg-gray-900 text-white p-4 rounded-lg">
  <h1 className="text-2xl font-bold">Welcome</h1>
</div>

// ❌ WRONG
<div class="custom-card"> // Wrong prop name
  <h1 class="title">Welcome</h1>
</div>

Dark Mode

  • Default to dark mode styling
  • Use Tailwind's dark mode utilities if needed
  • Maintain consistent dark theme
// ✅ CORRECT
<div className="bg-gray-900 text-gray-100">
  <p className="text-gray-300">Dark mode text</p>
</div>

Responsive Design

  • Use Tailwind responsive prefixes
  • Mobile-first approach
  • Test on multiple screen sizes
// ✅ CORRECT
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  {/* Cards */}
</div>

Icons

  • ALWAYS use inline SVG icons
  • NEVER use icon libraries like lucide-react, react-icons, or font-awesome
  • Create reusable SVG icon components when needed
  • SVGs should be defined as functional components
// ✅ CORRECT - Inline SVG as a component
const TrashIcon = () => (
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <polyline points="3 6 5 6 21 6"></polyline>
    <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
  </svg>
);

function DeleteButton() {
  return (
    <button className="flex items-center gap-2">
      <span className="w-4 h-4"><TrashIcon /></span>
      Delete
    </button>
  );
}

// ❌ WRONG - Using lucide-react
import { Trash2 } from "lucide-react";

function DeleteButton() {
  return (
    <button className="flex items-center gap-2">
      <Trash2 className="w-4 h-4" />
      Delete
    </button>
  );
}

// ❌ WRONG - Using react-icons
import { FaTrash } from "react-icons/fa";

function DeleteButton() {
  return (
    <button className="flex items-center gap-2">
      <FaTrash className="w-4 h-4" />
      Delete
    </button>
  );
}

Why inline SVGs?

  1. No dependencies: Reduces bundle size and eliminates external dependencies
  2. Full control: You can customize SVG properties directly in JSX
  3. Performance: No runtime overhead from library wrappers
  4. Consistency: All icons follow to same pattern and styling
  5. TypeScript support: Full type safety without any issues

Routing Rules

React Router

  • Use react-router-dom for navigation
  • Define routes in frontend/src/main.jsx
  • Use <Link> for navigation (not <a>)
// ✅ CORRECT
import { Link } from "react-router-dom"

function Navbar() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/products">Products</Link>
      <Link to="/cart">Cart</Link>
    </nav>
  )
}

Protected Routes

  • Use context to check authentication
  • Redirect to login if not authenticated
  • Store JWT in context
// ✅ CORRECT
import { useContext } from "react"
import { Navigate } from "react-router-dom"
import { AppContext } from "../context/AppContext"

function ProtectedRoute({ children }) {
  const { token } = useContext(AppContext)
  
  if (!token) {
    return <Navigate to="/login" />
  }
  
  return children
}

Form Rules

Form Handling

  • Use controlled components for forms
  • Store form state in useState
  • Handle onChange events properly
// ✅ CORRECT
function LoginForm() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  
  const handleSubmit = (e) => {
    e.preventDefault()
    // Handle submission
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="border rounded p-2"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        className="border rounded p-2"
      />
      <button type="submit" className="bg-blue-500 text-white p-2">
        Login
      </button>
    </form>
  )
}

Form Validation

  • Validate form data before submission
  • Show error messages to users
  • Disable submit button during submission
// ✅ CORRECT
function RegisterForm() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)
  
  const handleSubmit = async (e) => {
    e.preventDefault()
    setError("")
    
    // Validation
    if (!email || !password) {
      setError("Please fill in all fields")
      return
    }
    
    if (password.length < 6) {
      setError("Password must be at least 6 characters")
      return
    }
    
    setLoading(true)
    try {
      // Submit form
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      {error && <div className="text-red-500">{error}</div>}
      <button type="submit" disabled={loading} className="...">
        {loading ? "Submitting..." : "Register"}
      </button>
    </form>
  )
}

Error Handling Rules

Error Boundaries

  • Use error boundaries for component errors
  • Display user-friendly error messages
  • Log errors for debugging

API Errors

  • Catch errors from useApi hook
  • Display error messages to users
  • Use try-catch for async operations
// ✅ CORRECT
function Products() {
  const { api } = useApi()
  const [error, setError] = useState(null)
  
  const loadProducts = async () => {
    try {
      const response = await api.get("/api/products")
      setProducts(response.data)
    } catch (err) {
      setError("Failed to load products")
      console.error(err)
    }
  }
  
  if (error) {
    return <div className="text-red-500">{error}</div>
  }
  
  // Render products
}

Performance Rules

Memoization

  • Use React.memo() for expensive components
  • Use useMemo() for expensive calculations
  • Use useCallback() for stable function references
// ✅ CORRECT
import React, { useMemo, useCallback } from "react"

function ExpensiveComponent({ items }) {
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a.name.localeCompare(b.name))
  }, [items])
  
  const handleClick = useCallback((id) => {
    console.log("Clicked:", id)
  }, [])
  
  return (
    <div>
      {sortedItems.map(item => (
        <div key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </div>
      ))}
    </div>
  )
}

export default React.memo(ExpensiveComponent)

Lazy Loading

  • Use React.lazy() for code splitting
  • Use Suspense for loading states
  • Lazy load routes and heavy components
// ✅ CORRECT
import { lazy, Suspense } from "react"

const Products = lazy(() => import("./pages/Products"))
const Cart = lazy(() => import("./pages/Cart"))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/products" element={<Products />} />
        <Route path="/cart" element={<Cart />} />
      </Routes>
    </Suspense>
  )
}

File Structure for New Features

When adding new features, follow this structure:

frontend/src/
├── components/
│   └── feature/
│       ├── FeatureList.jsx
│       └── FeatureCard.jsx
├── pages/
│   └── FeaturePage.jsx
├── context/
│   └── FeatureContext.jsx (if needed)
├── hooks/
│   └── useFeature.js (if needed)
└── services/
    └── featureService.js (if needed)

Common Patterns

Creating a New Component

import React, { useState } from "react"
import PropTypes from "prop-types"

function NewComponent({ title, onAction }) {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <div className="bg-gray-800 p-4 rounded">
      <h2 className="text-xl font-bold text-white">{title}</h2>
      <button 
        onClick={() => setIsOpen(!isOpen)}
        className="bg-blue-500 text-white px-4 py-2 rounded"
      >
        {isOpen ? "Close" : "Open"}
      </button>
      {isOpen && (
        <div className="mt-4">
          <p>Content goes here</p>
          <button onClick={onAction}>Action</button>
        </div>
      )}
    </div>
  )
}

NewComponent.propTypes = {
  title: PropTypes.string.isRequired,
  onAction: PropTypes.func.isRequired
}

export default NewComponent

Creating a New Page

import React, { useEffect } from "react"
import useApi from "../hooks/useApi"

function NewPage() {
  const { api } = useApi()
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await api.get("/api/endpoint")
        setData(response.data)
      } catch (err) {
        setError("Failed to load data")
      } finally {
        setLoading(false)
      }
    }
    
    fetchData()
  }, [api])
  
  if (loading) {
    return <div className="text-white">Loading...</div>
  }
  
  if (error) {
    return <div className="text-red-500">{error}</div>
  }
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold text-white mb-4">
        New Page
      </h1>
      {/* Page content */}
    </div>
  )
}

export default NewPage

Creating a New Context

import React, { createContext, useContext, useState } from "react"

const NewContext = createContext(null)

export function NewContextProvider({ children }) {
  const [value, setValue] = useState(null)
  
  return (
    <NewContext.Provider value={{ value, setValue }}>
      {children}
    </NewContext.Provider>
  )
}

export function useNewContext() {
  const context = useContext(NewContext)
  if (!context) {
    throw new Error("useNewContext must be used within NewContextProvider")
  }
  return context
}

DO NOT DO List

NEVER use Zustand, Redux, or other state management NEVER use axios directly (always use useApi hook) NEVER use class components NEVER use class attribute (use className) NEVER forget key prop in lists NEVER ignore useEffect dependencies NEVER use this.state or this.props NEVER create custom CSS files (use Tailwind) NEVER use single quotes for strings NEVER forget error handling NEVER hardcode API URLs NEVER skip PropTypes validation NEVER use <a> for navigation (use <Link>) NEVER use icon libraries like lucide-react, react-icons, or font-awesome (always use inline SVGs)

Checklist Before Committing

  • Component uses functional syntax with hooks
  • All API calls use useApi hook
  • State uses Context API (not Redux/Zustand)
  • All props have PropTypes validation
  • Tailwind classes used for styling
  • Dark mode styling applied
  • Responsive design considered
  • Error handling implemented
  • Loading states added
  • Keys provided for list items
  • useEffect dependencies correct
  • Code follows project conventions
  • Files properly named (kebab-case)