Agent skill

pricing-app-frontend

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

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/pricing-app-frontend

Metadata

Additional technical details for this skill

scope
[
    "frontend",
    "root"
]
author
pricing-app
version
1.0.0
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"
]

SKILL.md

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

jsx
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

jsx
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)

jsx
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

css
/* 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

jsx
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>
  );
}
css
/* 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

js
// 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

bash
# 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:

jsx
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:

jsx
{
  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:

jsx
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:

jsx
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:

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

/* 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)

/* Accent */
--cf-accent-blue: #3b82f6
--cf-accent-blue-hover: #60a5fa

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

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

QA CHECKLIST

  • 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)
  • Dark mode works (test ThemeContext)
  • Permissions checked where needed
  • Alt text on images
  • Semantic HTML used
  • Cloudflare components use correct tokens (--cf-*)

REFERENCES

External

Internal

Didn't find tool you were looking for?

Be as detailed as possible for better results