agentskills.codes
RE

react-feature-frontend

>

Install

mkdir -p .claude/skills/react-feature-frontend && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16929" && unzip -o skill.zip -d .claude/skills/react-feature-frontend && rm skill.zip

Installs to .claude/skills/react-feature-frontend

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Use this skill whenever creating or modifying frontend code in the my-schedule project. Covers feature-based folder structure, React Query, Zustand, component patterns, and service layer. Trigger on: "create page", "create component", "create feature", "add hook", "add form", or any task touching apps/web/src/.
312 charsno explicit “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

About this skill

React Frontend — my-schedule

This skill defines how all frontend code must be written in this project. Stack: React 18 + TypeScript + Vite + Tailwind CSS + React Query + Zustand.


Folder Structure — Feature-based

All code lives inside a feature folder. Features map 1:1 to domain concepts.

src/
├── features/
│   ├── appointments/
│   │   ├── components/          ← componentes exclusivos desta feature
│   │   │   ├── AppointmentCard.tsx
│   │   │   ├── AppointmentStatusBadge.tsx
│   │   │   └── CancelAppointmentModal.tsx
│   │   ├── hooks/               ← React Query hooks desta feature
│   │   │   ├── useAppointments.ts
│   │   │   └── useCancelAppointment.ts
│   │   ├── services/            ← chamadas HTTP desta feature
│   │   │   └── appointments.service.ts
│   │   ├── types.ts             ← tipos locais da feature
│   │   └── index.ts             ← exporta apenas o que outras features precisam
│   │
│   ├── booking/                 ← fluxo público de agendamento (multi-step)
│   │   ├── components/
│   │   │   ├── StepProfessional.tsx
│   │   │   ├── StepService.tsx
│   │   │   ├── StepSlot.tsx
│   │   │   └── StepIdentify.tsx
│   │   ├── hooks/
│   │   │   ├── useAvailableSlots.ts
│   │   │   └── useCreateBooking.ts
│   │   ├── services/
│   │   │   └── booking.service.ts
│   │   ├── store.ts             ← Zustand store local da feature (multi-step state)
│   │   └── index.ts
│   │
│   ├── professionals/
│   ├── services/                ← (serviços do salão)
│   ├── availability/
│   └── auth/
│
├── pages/                       ← apenas composição de features, sem lógica
│   ├── admin/
│   │   ├── AppointmentsPage.tsx
│   │   └── ProfessionalsPage.tsx
│   ├── public/
│   │   └── BookingPage.tsx
│   └── customer/
│       └── MyAppointmentsPage.tsx
│
├── shared/                      ← código genuinamente compartilhado
│   ├── components/
│   │   ├── ui/                  ← Button, Input, Modal, Badge, Avatar
│   │   └── layout/              ← AdminLayout, PublicLayout, Sidebar
│   ├── hooks/
│   │   └── useDebounce.ts
│   ├── lib/
│   │   ├── api.ts               ← instância Axios configurada
│   │   └── formatters.ts        ← moeda, data, telefone
│   └── types/
│       └── common.ts            ← Paginated<T>, ApiError, etc.
│
└── stores/
    └── auth.store.ts            ← único store global: token + usuário

The rule: if a component, hook, or service is used by only one feature, it lives inside that feature. It only moves to shared/ when a second feature needs it.


Service Layer

Each feature has its own service file that wraps the API calls. No fetch or axios calls inside components or hooks directly.

// features/appointments/services/appointments.service.ts
import { api } from '@/shared/lib/api';
import type { Appointment, CreateAppointmentData } from '../types';
import type { Paginated } from '@/shared/types/common';

export const appointmentsService = {
  getMany: async (params: { page?: number; limit?: number }): Promise<Paginated<Appointment>> => {
    const { data } = await api.get('/appointments', { params });
    return data;
  },

  getById: async (id: string): Promise<Appointment> => {
    const { data } = await api.get(`/appointments/${id}`);
    return data;
  },

  create: async (payload: CreateAppointmentData): Promise<Appointment> => {
    const { data } = await api.post('/appointments', payload);
    return data;
  },

  confirm: async (id: string): Promise<Appointment> => {
    const { data } = await api.patch(`/appointments/${id}/confirm`);
    return data;
  },

  cancel: async (id: string, reason?: string): Promise<Appointment> => {
    const { data } = await api.patch(`/appointments/${id}/cancel`, { reason });
    return data;
  },
};

Rules:

  • Services are plain objects with async methods — not classes, not hooks.
  • Always destructure { data } from axios response — never return the full AxiosResponse.
  • URL paths match the backend exactly — no ad-hoc string interpolation outside service files.

React Query Hooks

Each feature has one file per query or mutation. Hooks wrap the service and configure caching.

// features/appointments/hooks/useAppointments.ts
import { useQuery } from '@tanstack/react-query';
import { appointmentsService } from '../services/appointments.service';

export const appointmentKeys = {
  all: ['appointments'] as const,
  lists: () => [...appointmentKeys.all, 'list'] as const,
  list: (params: object) => [...appointmentKeys.lists(), params] as const,
  detail: (id: string) => [...appointmentKeys.all, 'detail', id] as const,
};

export function useAppointments(params: { page?: number; limit?: number } = {}) {
  return useQuery({
    queryKey: appointmentKeys.list(params),
    queryFn: () => appointmentsService.getMany(params),
  });
}

export function useAppointment(id: string) {
  return useQuery({
    queryKey: appointmentKeys.detail(id),
    queryFn: () => appointmentsService.getById(id),
    enabled: !!id,
  });
}
// features/appointments/hooks/useCancelAppointment.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { appointmentsService } from '../services/appointments.service';
import { appointmentKeys } from './useAppointments';

export function useCancelAppointment() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, reason }: { id: string; reason?: string }) =>
      appointmentsService.cancel(id, reason),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: appointmentKeys.lists() });
    },
  });
}

Rules:

  • Always export a keys object from the hook file — used to invalidate queries correctly.
  • enabled: !!id on queries that depend on a param that might be undefined.
  • staleTime is set explicitly when the data has a clear freshness requirement:
    • Slots disponíveis → staleTime: 30_000 (30s — mudam rápido)
    • Lista de profissionais → staleTime: 5 * 60_000 (5min — quase estática)
  • Mutations always invalidate the relevant list queries on onSuccess.
  • One hook file per concern — don't mix queries and mutations in the same file.

Component Pattern

// features/appointments/components/AppointmentCard.tsx
import { formatDate, formatCurrency } from '@/shared/lib/formatters';
import { AppointmentStatusBadge } from './AppointmentStatusBadge';
import { useCancelAppointment } from '../hooks/useCancelAppointment';
import type { Appointment } from '../types';

interface AppointmentCardProps {
  appointment: Appointment;
  onCancelSuccess?: () => void;
}

export function AppointmentCard({ appointment, onCancelSuccess }: AppointmentCardProps) {
  const { mutate: cancel, isPending } = useCancelAppointment();

  function handleCancel() {
    cancel(
      { id: appointment.id },
      { onSuccess: onCancelSuccess },
    );
  }

  return (
    <div className="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm">
      <div className="flex items-start justify-between gap-4">
        <div>
          <p className="font-medium text-zinc-900">{appointment.professional.name}</p>
          <p className="text-sm text-zinc-500">{appointment.service.name}</p>
        </div>
        <AppointmentStatusBadge status={appointment.status} />
      </div>

      <div className="mt-3 flex items-center justify-between">
        <p className="text-sm text-zinc-600">{formatDate(appointment.startTime)}</p>
        <p className="text-sm font-medium text-zinc-900">
          {formatCurrency(appointment.service.price)}
        </p>
      </div>

      {appointment.status === 'pending' || appointment.status === 'confirmed' ? (
        <button
          onClick={handleCancel}
          disabled={isPending}
          className="mt-3 w-full rounded-lg border border-red-200 py-1.5 text-sm text-red-600 hover:bg-red-50 disabled:opacity-50"
        >
          {isPending ? 'Cancelando...' : 'Cancelar agendamento'}
        </button>
      ) : null}
    </div>
  );
}

Rules:

  • Components are named exports — never default export.
  • Props interface always named <ComponentName>Props, defined in the same file.
  • Hooks are called at the top of the component — never inside conditionals.
  • No any types — use the feature's types.ts or @/shared/types/common.ts.
  • Tailwind only — no inline style={{}} except for dynamic values Tailwind can't handle.
  • Conditional rendering uses ternary or condition && <El /> — never nested if/else inside JSX.

Feature-local Zustand Store

Only use a local Zustand store when a feature has multi-step or complex UI state that doesn't belong on the server. The booking flow is the main example.

// features/booking/store.ts
import { create } from 'zustand';
import type { Professional } from '@/features/professionals/types';
import type { Service } from '@/features/services/types';

type BookingStep = 'professional' | 'service' | 'slot' | 'identify' | 'confirm';

interface BookingSlot {
  startTime: string;
  endTime: string;
}

interface BookingState {
  step: BookingStep;
  professional: Professional | null;
  service: Service | null;
  slot: BookingSlot | null;

  setStep: (step: BookingStep) => void;
  selectProfessional: (professional: Professional) => void;
  selectService: (service: Service) => void;
  selectSlot: (slot: BookingSlot) => void;
  reset: () => void;
}

const initialState = {
  step: 'professional' as BookingStep,
  professional: null,
  service: null,
  slot: null,
};

export const useBookingStore = create<BookingState>((set) => ({
  ...initialState,

  setStep: (step) => set({ step }),
  selectProfessional: (professional) => set({ professional, step: 'service' }),
  selectService: (service) => set({ service, step: 'slot' }),
  selectSlot: (slot) => set({ slot, step: 'identify' }),
  reset: () => set(initialState),
}));

Rules:

  • Zustand stores for UI state only — server data always in React Query.
  • S

Content truncated.

Search skills

Search the agent skills registry