Instruction file imported from Trustless-Work/trustlesswork-backoffice (
.cursor/rules/DAPP.mdc). Copyright stays with the author.
Global Development Context (Next.js Monorepo)
You are acting as a Senior Fullstack Developer working on a Next.js.
The main goal is to ensure high code quality, consistency, scalability, and reusability across the entire ecosystem.
π§ General Principles
- Strict TypeScript (
strict: true) - No usage of
anyβ full rules inTYPESCRIPT.mdc(advanced types, guards, discriminated unions) - Everything must be explicitly typed:
- Models
- Entities
- Payloads
- Responses
- Hooks
- Functions
- State
- Never leave unused variables, imports, or functions
- Always apply Prettier formatting
- Carefully analyze and respect all existing ESLint rules
- Strictly follow existing code patterns
- Maintain a minimalist approach aligned with the current UI design system
- Do not use unnecesary comments
π§© Architecture & Componentization
-
Highly componentized codebase
-
β Never create large, monolithic, or heavy components
-
β Prefer small, focused, and reusable components
-
UI components must:
- Focus only on rendering UI
- Contain minimal interaction logic
-
Reusable or complex logic (state + effects):
- Must be moved to custom hooks
-
Before creating:
- A component
- A hook
- A type
- A helper / util function (format, parse, validate, etc.)
π Always verify that it does not already exist
- If it exists β reuse it
- If it does not exist β create it following existing patterns
React UI file size (*.tsx in ui/, components/)
React UI component files must stay small and scannable. Line count includes co-located skeletons and private subcomponents in the same file.
| Limit | Lines | Action |
|---|---|---|
| Target | β€ 200 | Default β new and refactored components should land here |
| Hard max | 300 | Must not exceed β split before merging or continuing work |
When a file approaches ~200 lines or exceeds 300, extract in this order:
- Presentational chunks β sibling components in the same feature
ui/folder (e.g.MembersTable.tsx+MembersTableRow.tsx,VerifiedWalletsList.tsx) - State, effects, mutations, form wiring β
hooks/(see Forms Structure andFORMS.mdc) - Pure helpers (format, parse, validate, labels) β
src/helpers/or featureutils/when feature-only - Large skeletons β
*Skeleton.tsxin the same folder when they push the main file over the limit
Does not apply to non-UI files (hooks/, services/, schemas/, route handlers) β those follow single-responsibility by domain, not this JSX line cap.
Refactoring existing oversized files (e.g. >300 lines) is required when you touch them for a meaningful change β do not grow them further; extract as part of the same PR.
π§ Helpers & Utils (src/helpers)
Single source of truth for pure, reusable functions (formatting, parsing, validation, display labels, etc.) that are not tied to a single feature.
- Before creating any helper (
formatX,parseY,getZLabel, etc.):- Search
src/helpers/β check existing files and exports - If it exists β import and reuse; do not redefine it locally
- If it does not exist β add it to
src/helpers/:- Extend an existing file when the function fits the same domain (e.g.
format.helper.ts,validators.helper.ts) - Create a new
*.helper.tsfile when the domain is distinct or the file would grow too large
- Extend an existing file when the function fits the same domain (e.g.
- Search
- β Never define inline helpers or duplicate the same logic in components, hooks, services, or feature
utils/folders when the function is (or could be) shared - β
Feature-scoped
utils/is only for logic exclusive to that feature and unlikely to be reused elsewhere - File naming:
{domain}.helper.ts(e.g.format.helper.ts,user-display.helper.ts) - Import from
@/helpers/...β one export per function, named exports
βοΈ Components & Functions
- UI Components
- Must always use arrow functions
- Use named exports
export const Component = () => {};
- Non-UI functions (formatting, helpers, utils, etc.)
- Must use the
functionkeyword - Use named exports
- Must live in
src/helpers/unless strictly feature-specific (see Helpers & Utils above)export function formatX() {}
- Must use the
- Always use Shadcn UI components
- β Do not create custom components if an equivalent already exists
π Data Fetching & Rendering
- Use TanStack Query for data fetching
- Properly apply:
- Server-Side Rendering (SSR)
- Client-Side Rendering (CSR)
- Choose the rendering strategy based on:
- SEO
- Performance
- UX
- Avoid over-fetching and unnecessary re-renders
π Forms Structure & Patterns
-
Core Libraries:
react-hook-form- Form state managementzod- Schema validation@hookform/resolvers/zod- Integration between zod and react-hook-form
-
Base Components (from
/form.tsx):Form- Wrapper aroundFormProviderfrom react-hook-formFormField- Wrapper aroundControllerfor field managementFormItem- Field containerFormLabel- Field labelFormControl- Input control wrapperFormDescription- Optional field descriptionFormMessage- Error message display
-
File Structure Pattern:
features/[feature]/ βββ schemas/ β βββ [feature].schema.ts # Zod schema definition βββ hooks/ β βββ use[Feature].ts # Custom hook with useForm βββ ui/ β βββ [Feature]Form.tsx # Form component βββ services/ βββ [feature].service.ts # API calls -
Code Patterns:
- Schema Definition:
export const featureSchema = z.object({ field: z.string().min(1, "Field is required"), // ... }); export type FeatureFormData = z.infer<typeof featureSchema>; - Custom Hook:
const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema), defaultValues: { /* ... */ }, mode: "onChange", // Real-time validation }); const onSubmit = async (values) => { /* ... */ }; - Form Component:
<Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <FormField control={form.control} name="fieldName" render={({ field }) => ( <FormItem> <FormLabel>Label</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> </form> </Form>
- Schema Definition:
-
Common Features:
- Real-time validation (
mode: "onChange") - Separation of concerns: schema β hook β component
- Full TypeScript support with
z.infer<typeof schema> - Error handling with
FormMessage - Loading states and toast notifications with
sonner
- Real-time validation (
π¨ UI / UX Standards
- Everything must be:
- 100% responsive
- Using all Tailwind breakpoints
- 100% compatible with Light / Dark mode
- 100% responsive
- Maintain:
- Clean design
- Minimalism
- Visual consistency
- Respect existing UI patterns
- Do not introduce new styles unless strictly necessary
Responsive data tables (lists)
When building or refactoring a data table (TanStack Table, HTML Table, or similar) that shows multiple columns of text, badges, amounts, and actions:
- Mobile & small viewports (
defaultthroughmdbreakpoint): render a card list, not a horizontal scroll-only table. Use@components/uiCard(and related primitives) so each row is a scannable block: title or primary field in the header area, secondary fields in a 2-column label grid (same pattern as contributor My applications and maintainer UX bounties lists). - Tablet and desktop (
mdand up): keep the table as the primary layout (overflow-x-autoon the wrapper when needed). - Implementation pattern:
- Two sibling blocks: cards
className="md:hidden"and tableclassName="hidden md:block"(or equivalent), fed from the same row data (same query / same array). - Loading: show loading skeletons defined in the same component file, above the main export β see
SKELETONS.mdc. - Empty and error states stay a single block unless a design requires otherwise. Empty (no data) must use
NoDataβ see below.
- Two sibling blocks: cards
- DRY: extract shared cell content (e.g. actions, status badge) into small components reused by both the table row and the card footer/contentβavoid duplicating mutation handlers or button trees.
- Do not ship a wide multi-column table as the only representation on narrow screens without a card (or equally usable) alternative.
Loading skeletons
Structural fidelity, co-location, dual layouts, variants, and the review checklist live in SKELETONS.mdc. Always use @/components/ui/skeleton and mirror the loaded UI 1:1 β never generic muted blocks for structured screens.
Empty states (no data)
When a query, list, table, or section has finished loading and the result set is empty (zero items), always render NoData from @/components/shared/NoData.
- Do not invent one-off empty UIs: no inline
border-dasheddivs, plain<p>placeholders, or duplicated empty-state markup in feature components. - Before adding empty UI, check whether
NoDataalready fits; extend it only if the product need is genuinely shared across many screens. - Required:
titleβ short, user-facing headline (e.g. "No members yet"). - Optional:
description(helper text),icon(Lucide icon; defaults toInbox),actionLabel+onActionwhen the empty state should offer a primary action (e.g. "Add member"). - Loading and error states are separate: co-located skeletons (see
SKELETONS.mdc) while loading; error banners or retry UI on failure β do not useNoDatafor those. - Dual layouts (table + mobile cards): one shared
NoDatablock for both breakpoints, not two different empty messages.
import { UsersIcon } from "lucide-react";
import { NoData } from "@/components/shared/NoData";
{
!isLoading && !error && items.length === 0 ? (
<NoData
icon={UsersIcon}
title="No members yet"
description="Add a user from the directory to get started."
actionLabel="Add member"
onAction={() => setDialogOpen(true)}
/>
) : null;
}
π§ Senior Engineering Mindset
- Always think about:
- Scalability
- Maintainability
- Reusability
- Readability
- Prefer clear code over clever code
- Every technical decision must be justifiable
- Avoid logic and style duplication
- Ensure consistency across the entire monorepo