skills/react-arch/SKILL.md
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.
npx skillsauth add landim32/awesome-ai-skills react-archInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill defines the standard approach for scaffolding a complete frontend entity architecture in this project. It creates all necessary files following the established patterns: Types, Service, Context, Hook, and Provider registration.
Before creating the architecture, you MUST:
docs/src/types/, src/services/, src/contexts/, and src/hooks/ before creating anythingAlways create files in this exact order:
src/types/{entity}.tssrc/services/{entity}Service.tssrc/contexts/{Entity}Context.tsxsrc/hooks/use{Entity}.tssrc/main.tsxany type — all types must be explicitly defined.alert() or window.confirm() — use toast from sonner for notifications and ConfirmModal for confirmation dialogs.useCallback for all context methods to prevent unnecessary re-renders.loading, error, and main data state in every context.handleResponse method.getHeaders(true) from apiHelpers for authenticated requests.result.sucesso before updating state (API responses use Portuguese keys).File: src/types/{entity}.ts
/** {Entity} Types — Types for the {entity description} system */
// Enums (if needed)
export enum {Entity}StatusEnum {
Unknown = 0,
Active = 1,
Inactive = 2,
}
// Core Entity
/** Main {entity} information */
export interface {Entity}Info {
{entity}Id: number;
name: string;
// ... other fields with JSDoc comments
}
// DTOs
/** Data required to create a new {entity} (no ID) */
export interface {Entity}InsertInfo {
name: string;
// ... fields required for creation
}
/** Data required to update an existing {entity} (includes ID) */
export interface {Entity}UpdateInfo {
{entity}Id: number;
name: string;
// ... fields required for update
}
// API Response Types — always include sucesso, mensagem, erros (Portuguese keys)
export interface {Entity}ListResult {
{entities}: {Entity}Info[];
sucesso: boolean;
mensagem: string | null;
erros: string[] | null;
}
export interface {Entity}GetResult {
{entity}: {Entity}Info;
sucesso: boolean;
mensagem: string | null;
erros: string[] | null;
}
/** Status-only operation result (import from existing types file if already defined) */
export interface StatusResult {
sucesso: boolean;
mensagem: string;
erros: string[] | null;
}
Key conventions:
sucesso, mensagem, erros — Portuguese keys{entity}Id (e.g., clanId)| null, not optional ?StatusResult already exists in another types file, import it instead of redefiningFile: src/services/{entity}Service.ts
import type {
{Entity}ListResult, {Entity}GetResult,
{Entity}InsertInfo, {Entity}UpdateInfo, StatusResult,
} from '../types/{entity}';
import { getHeaders } from './apiHelpers';
const API_BASE = `${import.meta.env.VITE_GOBLIN_API_URL || 'http://localhost:4041'}/api/{entity-kebab}`;
interface {Entity}ServiceConfig {
onUnauthorized?: () => void;
}
/** {Entity} Service — Manages all API operations related to {entities} */
class {Entity}Service {
private config: {Entity}ServiceConfig;
constructor(config: {Entity}ServiceConfig = {}) {
this.config = config;
}
private async handleResponse<T>(response: Response): Promise<T> {
if (response.status === 401) {
this.config.onUnauthorized?.();
throw new Error('Unauthorized');
}
if (!response.ok) {
const error = await response.text();
throw new Error(error || 'Request failed');
}
return response.json();
}
/** List all {entities} */
async list(): Promise<{Entity}ListResult> {
const response = await fetch(`${API_BASE}/list`, { headers: getHeaders(true) });
return this.handleResponse<{Entity}ListResult>(response);
}
/** Get a {entity} by ID */
async getById(id: number): Promise<{Entity}GetResult> {
const response = await fetch(`${API_BASE}/getbyid/${id}`, { headers: getHeaders(true) });
return this.handleResponse<{Entity}GetResult>(response);
}
/** Create a new {entity} */
async insert(data: {Entity}InsertInfo): Promise<{Entity}GetResult> {
const response = await fetch(`${API_BASE}/insert`, {
method: 'POST', headers: getHeaders(true), body: JSON.stringify(data),
});
return this.handleResponse<{Entity}GetResult>(response);
}
/** Update an existing {entity} */
async update(data: {Entity}UpdateInfo): Promise<{Entity}GetResult> {
const response = await fetch(`${API_BASE}/update`, {
method: 'PUT', headers: getHeaders(true), body: JSON.stringify(data),
});
return this.handleResponse<{Entity}GetResult>(response);
}
/** Delete a {entity} by ID */
async delete(id: number): Promise<StatusResult> {
const response = await fetch(`${API_BASE}/delete/${id}`, {
method: 'DELETE', headers: getHeaders(true),
});
return this.handleResponse<StatusResult>(response);
}
}
export const {entity}Service = new {Entity}Service();
export default {Entity}Service;
Key conventions:
handleResponse that checks 401 and calls onUnauthorizedAPI_BASE uses VITE_GOBLIN_API_URL env var with http://localhost:4041 fallbackgetHeaders(true) for authenticated requestsFile: src/contexts/{Entity}Context.tsx
The context wraps the service and provides state management with three method categories: API Methods (direct service wrappers returning API results), State Management (handle loading/error, update local state), and optionally Utility Methods.
import { createContext, useState, useCallback, ReactNode } from 'react';
import { {entity}Service } from '../services/{entity}Service';
import type {
{Entity}Info, {Entity}ListResult, {Entity}GetResult,
{Entity}InsertInfo, {Entity}UpdateInfo, StatusResult,
} from '../types/{entity}';
interface {Entity}ContextType {
// State
{entities}: {Entity}Info[];
selected{Entity}: {Entity}Info | null;
loading: boolean;
error: string | null;
// API Methods (return API results for caller to check sucesso)
list{Entities}: () => Promise<{Entity}ListResult>;
get{Entity}ById: (id: number) => Promise<{Entity}GetResult>;
insert{Entity}: (data: {Entity}InsertInfo) => Promise<{Entity}GetResult>;
update{Entity}: (data: {Entity}UpdateInfo) => Promise<{Entity}GetResult>;
delete{Entity}: (id: number) => Promise<StatusResult>;
// State Management (return void, handle loading/error internally)
load{Entities}: () => Promise<void>;
refresh{Entities}: () => Promise<void>;
setSelected{Entity}: (item: {Entity}Info | null) => void;
clearError: () => void;
}
const {Entity}Context = createContext<{Entity}ContextType | undefined>(undefined);
export const {Entity}Provider = ({ children }: { children: ReactNode }) => {
const [{entities}, set{Entities}] = useState<{Entity}Info[]>([]);
const [selected{Entity}, setSelected{Entity}] = useState<{Entity}Info | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleError = (err: unknown): never => {
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
setError(errorMsg);
throw err;
};
// --- API Methods (direct service wrappers) ---
const list{Entities} = useCallback(async (): Promise<{Entity}ListResult> => {
try { setError(null); return await {entity}Service.list(); }
catch (err) { return handleError(err); }
}, []);
const get{Entity}ById = useCallback(async (id: number): Promise<{Entity}GetResult> => {
try { setError(null); return await {entity}Service.getById(id); }
catch (err) { return handleError(err); }
}, []);
const insert{Entity} = useCallback(async (data: {Entity}InsertInfo): Promise<{Entity}GetResult> => {
try {
setError(null);
const result = await {entity}Service.insert(data);
if (result.sucesso) await load{Entities}();
return result;
} catch (err) { return handleError(err); }
}, []);
const update{Entity} = useCallback(async (data: {Entity}UpdateInfo): Promise<{Entity}GetResult> => {
try {
setError(null);
const result = await {entity}Service.update(data);
if (result.sucesso) {
set{Entities}((prev) => prev.map((item) =>
item.{entity}Id === data.{entity}Id ? result.{entity} : item
));
if (selected{Entity}?.{entity}Id === data.{entity}Id) setSelected{Entity}(result.{entity});
}
return result;
} catch (err) { return handleError(err); }
}, [selected{Entity}]);
const delete{Entity} = useCallback(async (id: number): Promise<StatusResult> => {
try {
setError(null);
const result = await {entity}Service.delete(id);
if (result.sucesso) {
set{Entities}((prev) => prev.filter((item) => item.{entity}Id !== id));
if (selected{Entity}?.{entity}Id === id) setSelected{Entity}(null);
}
return result;
} catch (err) { return handleError(err); }
}, [selected{Entity}]);
// --- State Management ---
const load{Entities} = useCallback(async (): Promise<void> => {
try {
setLoading(true);
setError(null);
const result = await {entity}Service.list();
if (result.sucesso) set{Entities}(result.{entities});
else throw new Error(result.mensagem || 'Failed to load {entities}');
} catch (err) { handleError(err); }
finally { setLoading(false); }
}, []);
const refresh{Entities} = useCallback(async () => { await load{Entities}(); }, [load{Entities}]);
const clearError = useCallback(() => { setError(null); }, []);
const value: {Entity}ContextType = {
{entities}, selected{Entity}, loading, error,
list{Entities}, get{Entity}ById, insert{Entity}, update{Entity}, delete{Entity},
load{Entities}, refresh{Entities}, setSelected{Entity}, clearError,
};
return <{Entity}Context.Provider value={value}>{children}</{Entity}Context.Provider>;
};
export default {Entity}Context;
Key conventions:
handleError sets error state AND re-throws (returns never)result.sucesso before updating stateFile: src/hooks/use{Entity}.ts
import { useContext } from 'react';
import {Entity}Context from '../contexts/{Entity}Context';
/** Custom hook to access the {Entity} context. Throws if used outside {Entity}Provider. */
export const use{Entity} = () => {
const context = useContext({Entity}Context);
if (!context) throw new Error('use{Entity} must be used within a {Entity}Provider');
return context;
};
export default use{Entity};
Only add computed values/derived state if the user specifically requests it.
File: src/main.tsx
// Add import at the top with other provider imports
import { {Entity}Provider } from './contexts/{Entity}Context.tsx'
// Add to the provider chain based on dependencies:
// - If it depends on AuthContext → must be inside AuthProvider
// - If it depends on GoblinContext → must be inside GoblinProvider
// - If independent → place near the end, before App
Nesting rules:
AuthProvider is always the outermost (all contexts depend on auth)<App /> as possible unless it has dependentsCurrent provider order:
AuthProvider → FinanceProvider → GoblinProvider → TeamProvider →
GoboxProvider → AuctionProvider → GLogProvider → QuestProvider →
ItemProvider → ItemClaimProvider → MiningProvider → MapProvider →
TerritoryProvider → TerritoryEnemyProvider → ArenaProvider → App
| Item | Convention | Example |
|------|-----------|---------|
| Types file | src/types/{entity}.ts | src/types/clan.ts |
| Service file | src/services/{entity}Service.ts | src/services/clanService.ts |
| Service class | {Entity}Service | ClanService |
| Service instance | {entity}Service | clanService |
| Context file | src/contexts/{Entity}Context.tsx | src/contexts/ClanContext.tsx |
| Provider | {Entity}Provider | ClanProvider |
| Hook file | src/hooks/use{Entity}.ts | src/hooks/useClan.ts |
| Hook function | use{Entity} | useClan |
| Entity ID field | {entity}Id | clanId |
| List/Get result | {Entity}ListResult / {Entity}GetResult | ClanListResult / ClanGetResult |
| Insert/Update DTO | {Entity}InsertInfo / {Entity}UpdateInfo | ClanInsertInfo / ClanUpdateInfo |
src/types/{entity}.ts with all interfacessrc/services/{entity}Service.ts with class patternsrc/contexts/{Entity}Context.tsx with providersrc/hooks/use{Entity}.ts with null-checksrc/main.tsx in correct nesting positionsucesso, mensagem, erros fieldsuseCallbackhandleError pattern: sets error state + re-throwsany types, no alert(), no window.confirm()getHeaders(true) and VITE_GOBLIN_API_URL env varsrc/types/map.ts or other type files before redefining. Import from existing file if available.{entity}Id (camelCase), not {entity}_id or id.sucesso (not success), mensagem (not message), erros (not errors).onUnauthorized callback in config interface.tools
Guides how to integrate the zTools package for ChatGPT, DALL-E image generation, file upload (S3), slug generation, email sending, and document validation in a .NET 8 project. Use when the user wants to use AI features, upload files, generate slugs, send emails, or understand zTools integration.
documentation
Generates a comprehensive, standardized README.md for any project. Use when the user wants to create or regenerate a README file following the project's documentation standard.
development
Create modal dialogs in the frontend using a custom Modal component built on top of Radix UI Dialog. Use this skill whenever the user asks to create, add, or modify a modal, dialog, popup, or confirmation prompt in the React application.
development
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.