pricing-app-frontend

React frontend patterns for Pricing App - Zustand, CSS Modules, Tesla Design System, permissions

SKILL.md

GitHub

Pricing App Frontend - React + Zustand + CSS Modules


CRITICAL RULES - NON-NEGOTIABLE

React Imports

  • ALWAYS: import { useState, useEffect } from 'react'
  • NEVER: import React from 'react' or import * as React

Components

  • ALWAYS: Functional components with hooks
  • ALWAYS: Named imports for hooks
  • ALWAYS: Prop destructuring: function Button({ label, onClick })
  • NEVER: Class components
  • NEVER: Default export for multiple components in same file

State Management

  • ALWAYS: Zustand for global state (auth)
  • ALWAYS: React Context for theme, permissions
  • ALWAYS: Local state for component-specific data
  • NEVER: Lift state unnecessarily

Styling

  • ALWAYS: CSS Modules for component styles: import styles from './Component.module.css'
  • ALWAYS: Design tokens from design-tokens.css: var(--bg-primary), var(--text-primary)
  • ALWAYS: Tesla components when available (buttons-tesla.css, modals-tesla.css, table-tesla.css)
  • NEVER: Inline styles (except dynamic values)
  • NEVER: Hardcoded colors (use design tokens)
  • NEVER: Tailwind utilities (project uses CSS Modules)

API Calls

  • ALWAYS: Use axios from services/api.js
  • ALWAYS: Check token before API calls: localStorage.getItem('token')
  • ALWAYS: Handle loading states
  • ALWAYS: Show user feedback on errors
  • NEVER: Fetch without error handling

Icons & Visual Style

  • ALWAYS: Use lucide-react for all icons: import { Package, Check, X } from 'lucide-react'
  • ALWAYS: Subtle, minimal aesthetic โ€” clean and professional
  • NEVER: Use emoji as icons (๐Ÿ“ฆ, โœ…, โŒ, ๐Ÿ’ฐ, โšก, etc.) โ€” use lucide SVGs instead
  • NEVER: Use react-icons for new code โ€” standardized on lucide-react

Modals

  • ALWAYS: Modals close ONLY via X button or Cancel/Close button
  • NEVER: Add onClick to .modalOverlay to close โ€” users lose data clicking outside accidentally
  • NEVER: Use stopPropagation on .modalContent as a workaround for overlay click-to-close

Accessibility

  • ALWAYS: Alt text on images: <img src="logo.png" alt="Company logo" />
  • ALWAYS: Semantic HTML: <button> not <div onClick>
  • ALWAYS: ARIA labels for icon-only buttons: <button aria-label="Close modal">

PROJECT STRUCTURE

frontend/src/
โ”œโ”€โ”€ pages/                 # Full page components
โ”‚   โ”œโ”€โ”€ Productos.jsx
โ”‚   โ”œโ”€โ”€ Ventas.jsx
โ”‚   โ””โ”€โ”€ Admin.jsx
โ”œโ”€โ”€ components/            # Reusable components
โ”‚   โ”œโ”€โ”€ ModalTesla.jsx
โ”‚   โ”œโ”€โ”€ PricingModal.jsx
โ”‚   โ”œโ”€โ”€ Navbar.jsx
โ”‚   โ””โ”€โ”€ turbo/             # Domain-specific components
โ”œโ”€โ”€ contexts/              # React contexts
โ”‚   โ”œโ”€โ”€ ThemeContext.jsx   # Dark mode
โ”‚   โ””โ”€โ”€ PermisosContext.jsx # User permissions
โ”œโ”€โ”€ hooks/                 # Custom hooks
โ”‚   โ”œโ”€โ”€ useDebounce.js
โ”‚   โ”œโ”€โ”€ usePermisos.js
โ”‚   โ””โ”€โ”€ useServerPagination.js
โ”œโ”€โ”€ store/                 # Zustand stores
โ”‚   โ””โ”€โ”€ authStore.js       # Auth state
โ”œโ”€โ”€ services/              # API client
โ”‚   โ””โ”€โ”€ api.js             # Axios instance
โ””โ”€โ”€ styles/                # Global CSS, design tokens
    โ”œโ”€โ”€ design-tokens.css
    โ”œโ”€โ”€ buttons-tesla.css
    โ”œโ”€โ”€ modals-tesla.css
    โ””โ”€โ”€ table-tesla.css

PATTERNS

Functional Component with Hooks

import { useState, useEffect } from 'react';
import styles from './ProductosList.module.css';

export default function ProductosList({ onSelect }) {
  const [productos, setProductos] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchProductos();
  }, []);

  const fetchProductos = async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch('/api/productos');
      const data = await response.json();
      setProductos(data);
    } catch (err) {
      setError('Error al cargar productos');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <div className={styles.loading}>Cargando...</div>;
  if (error) return <div className={styles.error}>{error}</div>;

  return (
    <div className={styles.container}>
      {productos.map(p => (
        <div key={p.id} onClick={() => onSelect(p)}>
          {p.descripcion}
        </div>
      ))}
    </div>
  );
}

Using Zustand Store

import { create } from 'zustand';

export const useAuthStore = create((set) => ({
  user: null,
  token: localStorage.getItem('token'),
  
  setUser: (user) => set({ user }),
  
  setToken: (token) => {
    localStorage.setItem('token', token);
    set({ token });
  },
  
  logout: () => {
    localStorage.removeItem('token');
    set({ user: null, token: null });
  }
}));

// Usage in component
import { useAuthStore } from '@/store/authStore';

function Navbar() {
  const { user, logout } = useAuthStore();
  
  return (
    <nav>
      <span>{user?.nombre}</span>
      <button onClick={logout}>Salir</button>
    </nav>
  );
}

Using Context (Permissions)

import { createContext, useContext, useState, useEffect } from 'react';
import { useAuthStore } from '@/store/authStore';

const PermisosContext = createContext();

export function PermisosProvider({ children }) {
  const { user } = useAuthStore();
  const [permisos, setPermisos] = useState([]);

  useEffect(() => {
    if (user?.roles) {
      // Calculate permissions from roles
      const allPermisos = user.roles.flatMap(role => 
        PERMISOS_CATEGORIAS[role] || []
      );
      setPermisos([...new Set(allPermisos)]);
    }
  }, [user]);

  const tienePermiso = (categoria) => permisos.includes(categoria);

  return (
    <PermisosContext.Provider value={{ permisos, tienePermiso }}>
      {children}
    </PermisosContext.Provider>
  );
}

export const usePermisos = () => useContext(PermisosContext);

// Usage in component
function AdminPanel() {
  const { tienePermiso } = usePermisos();

  if (!tienePermiso('config')) {
    return <div>No tienes permiso</div>;
  }

  return <div>Panel de Admin</div>;
}

CSS Modules with Design Tokens

/* ProductosList.module.css */
.container {
  background: var(--bg-primary);
  color: var(--text-primary);
  padding: var(--spacing-md);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.loading {
  color: var(--text-secondary);
  text-align: center;
  padding: var(--spacing-lg);
}

.error {
  background: var(--error-bg);
  color: var(--error-text);
  padding: var(--spacing-sm);
  border-radius: var(--radius-sm);
}

/* Dark mode support (automatic via design tokens) */

Tesla Button Component

import styles from './Button.module.css';

export default function Button({ 
  label, 
  onClick, 
  variant = 'primary', 
  disabled = false 
}) {
  return (
    <button 
      className={`${styles.btnBase} ${styles[`btn${variant.charAt(0).toUpperCase() + variant.slice(1)}`]}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}
/* Button.module.css */
.btnBase {
  composes: btn-base from '../../styles/buttons-tesla.css';
}

.btnPrimary {
  composes: btn-primary from '../../styles/buttons-tesla.css';
}

.btnSecondary {
  composes: btn-secondary from '../../styles/buttons-tesla.css';
}

Custom Hook

// hooks/useDebounce.js
import { useState, useEffect } from 'react';

export function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchBar() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetchResults(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

NAMING CONVENTIONS

Entity Pattern Example
Component file PascalCase.jsx ProductosList.jsx
Hook file camelCase.js useDebounce.js
Utility file camelCase.js formatCurrency.js
CSS Module PascalCase.module.css ProductosList.module.css
CSS class camelCase .btnPrimary, .modalHeader

COMMON PITFALLS

Frontend

  • โŒ Don't use useEffect without dependencies array โ†’ Will cause infinite loops
  • โŒ Don't mutate state directly โ†’ Use setState functions
  • โŒ Don't forget to cleanup effects โ†’ Clear timers, unsubscribe
  • โŒ Don't store sensitive data in localStorage โ†’ Only JWT token
  • โŒ Don't use inline styles โ†’ Use CSS Modules with design tokens
  • โŒ Don't hardcode colors โ†’ Use var(--color-name)

COMMANDS

# Development
cd frontend
npm install
npm run dev

# Build
npm run build
npm run preview

# Linting
npm run lint


CLOUDFLARE DESIGN SYSTEM

Sidebar Navigation (Collapsible)

Components:

  • Sidebar.jsx: Main sidebar with 3 states (expanded/collapsed/hover-peek)
  • SidebarSection.jsx: Collapsible section with menu items
  • TopBar.jsx: Minimal top header
  • AppLayout.jsx: Layout wrapper

States:

  1. Expanded (pinned): 240px width, shows icons + text
  2. Collapsed (pinned): 64px width, shows only icons
  3. Hover-peek (temporary): Expands to 240px on hover when collapsed

Pattern:

import Sidebar from '@/components/Sidebar';
import TopBar from '@/components/TopBar';
import AppLayout from '@/components/AppLayout';

// En App.jsx o router
<AppLayout>
  <Outlet />
</AppLayout>

Adding menu items: Edit Sidebar.jsx and add to menuSections array:

{
  id: 'productos',
  title: 'Productos',
  icon: '๐Ÿ“ฆ',
  defaultOpen: true,
  items: [
    { label: 'Productos', path: '/productos', permiso: 'productos.ver' },
    // ...
  ],
}

CloudflareCard Component

Variants:

  • default: Standard padding (20px)
  • compact: Small padding (16px)
  • metric: For dashboard metrics

Pattern:

import CloudflareCard, { MetricCard } from '@/components/CloudflareCard';

// Standard card
<CloudflareCard 
  title="Dominios"
  action={<button>+ Agregar</button>}
>
  <p>Content here</p>
</CloudflareCard>

// Metric card for dashboards
<MetricCard
  label="Solicitudes HTTP"
  value="138,3k"
  trend="+3.65%"
  trendDirection="up"
  chart={<Sparkline data={[...]} />}
  info="Informaciรณn adicional"
/>

Styling:

  • Use var(--cf-card-bg), var(--cf-card-border)
  • Compact by default (no grotesco padding)
  • Subtle shadows: var(--cf-card-shadow)

AlertBanner Component

Variants:

  • info: Blue background (default)
  • warning: Orange background
  • success: Green background
  • error: Red background

Pattern:

import AlertBanner, { AlertBannerContainer } from '@/components/AlertBanner';

<AlertBannerContainer>
  <AlertBanner
    id="unique-banner-id"
    variant="info"
    message="Mensaje informativo aquรญ"
    action={{
      label: 'Ver mรกs',
      onClick: () => navigate('/page')
    }}
    dismissible={true}
  />
</AlertBannerContainer>

Features:

  • Persists dismiss state in localStorage
  • Optional action button
  • Can be non-dismissible with dismissible={false}
  • Use persistent={true} to ignore localStorage

NOTE: AlertBanners will be managed from backend admin panel (dynamic system TBD).


Design Tokens - Cloudflare

Theme-aware tokens (adapt to light/dark mode):

Dark Mode (default):

/* Backgrounds */
--cf-bg-app: #000000
--cf-bg-sidebar: #0a0a0a
--cf-bg-card: #181818
--cf-bg-hover: #1f1f1f
--cf-bg-topbar: #0a0a0a

/* Borders */
--cf-border-subtle: #1a1a1a
--cf-border-default: #2a2a2a

/* Text */
--cf-text-primary: #ffffff
--cf-text-secondary: rgba(255, 255, 255, 0.7)
--cf-text-tertiary: rgba(255, 255, 255, 0.5)
--cf-text-muted: rgba(255, 255, 255, 0.4)

Light Mode:

/* Backgrounds */
--cf-bg-app: #f9fafb
--cf-bg-sidebar: #ffffff
--cf-bg-card: #ffffff
--cf-bg-hover: #f3f4f6
--cf-bg-topbar: #ffffff

/* Borders */
--cf-border-subtle: #e5e7eb
--cf-border-default: #d1d5db

/* Text */
--cf-text-primary: #111827
--cf-text-secondary: #4b5563
--cf-text-tertiary: #6b7280
--cf-text-muted: #9ca3af

Theme-independent tokens:

/* Accent - Same in both themes */
--cf-accent-blue: #3b82f6
--cf-accent-blue-hover: #60a5fa

/* Layout */
--cf-sidebar-width-expanded: 240px
--cf-sidebar-width-collapsed: 64px
--cf-topbar-height: 56px

/* Typography */
--font-extralight: 200
--font-light: 300
--font-normal: 400
--font-medium: 500
--font-semibold: 600
--font-bold: 700

/* Cards */
--cf-card-padding-compact: 16px
--cf-card-padding-default: 20px
--cf-card-radius: 8px

Typography Guidelines:

  • Section headers (Sidebar): font-size 12px, font-weight semibold (600), letter-spacing 0.08em, uppercase
  • Menu items (Sidebar): font-size 13px, font-weight extralight (200), color secondary
  • Active menu item: font-weight normal (400), color accent-blue
  • TopBar elements: font-weight normal (400), color secondary

Light Mode Support

Theme Toggle:

  • ThemeContext provides theme and toggleTheme()
  • Persisted in localStorage as data-theme on <html> element
  • All --cf-* tokens adapt automatically

Component Overrides: When tokens alone aren't enough, use :global([data-theme="light"]) in CSS Modules:

/* Dark mode (default) */
.logoIcon {
  opacity: 1;
}

/* Light mode override */
:global([data-theme="light"]) .logoIcon {
  filter: invert(1) brightness(0); /* White logo โ†’ Black logo */
}

Common light mode patterns:

  • Logo inversion: filter: invert(1) brightness(0) for whiteโ†’black
  • Avatar backgrounds: Use solid colors instead of rgba(255,255,255,0.x)
  • Alert banners: Override text/button colors with solid dark colors
  • Hover states: Use rgba(17, 24, 39, 0.1) instead of rgba(255, 255, 255, 0.05)

Testing checklist:

  • Toggle between light/dark multiple times
  • Check text contrast (WCAG AA minimum)
  • Verify logo visibility
  • Test hover states on buttons/links
  • Check alert banners readability

QA CHECKLIST

Code Quality:

  • Components use functional syntax with hooks
  • No import React statements
  • Proper error handling on API calls
  • Loading states shown to user
  • CSS Modules used (no inline styles)
  • Design tokens used (no hardcoded colors)
  • Cloudflare components use correct tokens (--cf-*)

Accessibility:

  • Alt text on images
  • Semantic HTML used
  • ARIA labels on icon-only buttons

Permissions:

  • Permissions checked where needed
  • ProtectedRoute wraps sensitive pages

Theme Support:

  • Light AND dark mode tested
  • Text contrast meets WCAG AA
  • Logo visible in both themes
  • No hardcoded rgba() colors (use tokens)
  • Alert banners readable in both themes

Layout:

  • Sidebar collapses/expands correctly
  • TopBar adjusts to sidebar state
  • AlertBanner repositions with sidebar
  • Mobile responsive (test at 768px breakpoint)

REFERENCES

External

Internal