| name | pricing-app-frontend |
| description | React frontend patterns for Pricing App - Zustand, CSS Modules, Tesla Design System, permissions |
| license | MIT |
| metadata | {"author":"pricing-app","version":"1.0.0","scope":["frontend","root"],"auto_invoke":["Creating/modifying React components","Working with Zustand store","Styling with CSS Modules or Tesla Design","Implementing dark mode","Using PermisosContext or ThemeContext","Creating custom hooks"]} |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash |
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
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 ;
(error) ;
(
);
}
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 });
}
}));
import { useAuthStore } from '@/store/authStore';
function Navbar() {
const { user, logout } = useAuthStore();
return (
<nav>
<span>{user?.nombre}</span>
<button =>Salir
);
}
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) {
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 = () => ();
() {
{ tienePermiso } = ();
(!()) {
;
}
;
}
CSS Modules with Design Tokens
.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);
}
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>
);
}
.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
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;
}
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
cd frontend
npm install
npm run dev
npm run build
npm run preview
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:
- Expanded (pinned): 240px width, shows icons + text
- Collapsed (pinned): 64px width, shows only icons
- 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';
<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';
<CloudflareCard
title="Dominios"
action={<button>+ Agregar</button>}
>
<p>Content here</p>
</CloudflareCard>
<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
Available tokens:
--cf-bg-app: #000000
--cf-bg-sidebar: #0a0a0a
--cf-bg-card: #181818
--cf-bg-hover: #1f1f1f
--cf-border-subtle: #1a1a1a
--cf-border-default: #2a2a2a
--cf-text-primary: #ffffff
--cf-text-secondary: rgba(255, 255, 255, 0.7)
--cf-text-tertiary: rgba(255, 255, 255, 0.5)
--cf-accent-blue: #3b82f6
--cf-accent-blue-hover: #60a5fa
--cf-sidebar-width-expanded: 240px
--cf-sidebar-width-collapsed: 64px
--cf-topbar-height: 56px
--cf-card-padding-compact: 16px
--cf-card-padding-default: 20px
--cf-card-radius: 8px
QA CHECKLIST
REFERENCES
External
Internal