feat: ui-ux mockups for backoffice application#65
Conversation
|
@DanielCarrillo127 is attempting to deploy a commit to the Trustless Work Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR introduces a comprehensive campaigns management system for the backoffice tokenization platform, including multi-page dashboard routing, campaign CRUD with multi-step creation flows, loan milestone management, ROI tracking and dialogs, sidebar navigation, and associated hooks, services, and utility functions. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CreateCampaignStepper as CreateCampaignStepper
participant useCreateCampaign as useCreateCampaign Hook
participant Form as React Hook Form
participant API as campaigns.api
participant Router as Next Router
User->>CreateCampaignStepper: Opens campaign creation
CreateCampaignStepper->>useCreateCampaign: Initialize hook
useCreateCampaign->>Form: Setup multi-step form (3 steps)
Note over User,Router: Step 1: Campaign Basics
User->>CreateCampaignStepper: Fill basics (name, description, duration, ROI)
CreateCampaignStepper->>Form: Update form values
User->>CreateCampaignStepper: Click Next
CreateCampaignStepper->>useCreateCampaign: nextStep()
Note over User,Router: Step 2: Escrow Configuration
User->>CreateCampaignStepper: Enter target amount
CreateCampaignStepper->>Form: Update escrow values
CreateCampaignStepper->>CreateCampaignStepper: Calculate total commitment
User->>CreateCampaignStepper: Click Next
CreateCampaignStepper->>useCreateCampaign: nextStep()
Note over User,Router: Step 3: Token Creation
User->>CreateCampaignStepper: Select token asset, enter investment amount
CreateCampaignStepper->>Form: Update token values
User->>CreateCampaignStepper: Click Submit
CreateCampaignStepper->>useCreateCampaign: onSubmit()
useCreateCampaign->>Form: Validate all steps
Form-->>useCreateCampaign: Form is valid
useCreateCampaign->>API: createCampaign(formData)
API-->>useCreateCampaign: Campaign created
useCreateCampaign->>Router: Navigate to /campaigns
Router-->>User: Campaign list view
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related Issues
Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can enforce grammar and style rules using `languagetool`.Configure the |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (16)
apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx (2)
14-22: Use shared Button component from@repo/ui.The native
<button>element should be replaced with the shared Button component to maintain consistency across the monorepo and leverage shared styling/behavior.♻️ Proposed fix using `@repo/ui` Button
"use client"; +import { Button } from "@repo/ui/button"; + interface ClaimRoiButtonProps { campaignId: string; disabled?: boolean; } export function ClaimRoiButton({ campaignId, disabled }: ClaimRoiButtonProps) { const handleClaim = () => { // TODO: implement claim ROI transaction console.log("Claiming ROI for campaign:", campaignId); }; return ( - <button - type="button" + <Button onClick={handleClaim} disabled={disabled} - className="h-10 rounded-xl bg-accent-orange px-5 text-sm font-medium text-white hover:bg-accent-orange-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + className="h-10 rounded-xl bg-accent-orange px-5 text-sm font-medium text-white hover:bg-accent-orange-hover transition-colors" > Reclamar ROI - </button> + </Button> ); }As per coding guidelines: "Import shared UI components from
@repo/uipackage (button, card, etc.)"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx` around lines 14 - 22, Replace the native <button> with the shared Button component from `@repo/ui`: import Button from '@repo/ui' (or named export used in the codebase), then render <Button> with the same props as the current element—pass type="button", onClick={handleClaim}, disabled={disabled}, preserve the className or map styling to Button props, and keep the button label "Reclamar ROI"; update any prop names to match Button's API if it differs (e.g., className -> className or variant/size) and remove the native button element.
9-12: Placeholder implementation noted.The TODO indicates a pending claim ROI transaction. Per the coding guidelines, the actual Soroban transaction should be built server-side in
src/lib/sorobanClient.tsand invoked via an API route from this component.Would you like me to help scaffold the API route and service layer for the claim ROI transaction, or open an issue to track this task?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx` around lines 9 - 12, The placeholder handleClaim in the ClaimRoiButton component should call a server-side API that builds and submits the Soroban claim ROI transaction instead of doing it in the client; implement a new exported function claimRoi(campaignId, walletInfo) in src/lib/sorobanClient.ts that constructs/signs/submits the Soroban transaction and returns the submission result, add an API route (e.g. POST /api/campaigns/:id/claim-roi) that invokes sorobanClient.claimRoi and returns success/error, then update the component's handleClaim to call that API, handle loading/error states and surface errors to the user; ensure errors from sorobanClient.claimRoi are logged and returned to the client for display.apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts (1)
3-5: Clamp progress on both ends.This only caps at
100. If mock/API data comes in withraisedAmount < 0ortargetAmount <= 0, the UI can render a negative progress value. Clamping to[0, 100]makes the mapper safer for bad or partial data.Suggested change
export function mapCampaignProgress(campaign: Campaign): number { - if (campaign.targetAmount === 0) return 0; - return Math.min((campaign.raisedAmount / campaign.targetAmount) * 100, 100); + if (campaign.targetAmount <= 0) return 0; + const progress = (campaign.raisedAmount / campaign.targetAmount) * 100; + return Math.min(Math.max(progress, 0), 100); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts` around lines 3 - 5, mapCampaignProgress currently only caps the computed percent at 100 but can return negative or NaN for bad data; update the function (mapCampaignProgress) to treat non-positive targetAmount as zero (return 0) and clamp the computed value between 0 and 100 (use Math.min/Math.max or an equivalent clamp on (campaign.raisedAmount / campaign.targetAmount) * 100) so the returned progress is always within [0,100]; reference campaign.targetAmount and campaign.raisedAmount when implementing the checks.apps/backoffice-tokenization/src/components/layout/app-header.tsx (1)
1-11: Move this into a feature-owned layout module.This is app-specific dashboard UI, but it lives under
src/components/layout. Please colocate it under a layout/dashboard feature (for example,src/features/layout/components) so the new backoffice surface stays aligned with the repo’s feature-based structure.As per coding guidelines: Organize Next.js frontends using feature-based folder structure in
src/features/.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/components/layout/app-header.tsx` around lines 1 - 11, Move the AppHeader component out of the generic components layout folder into the layout/dashboard feature's components module so it is colocated with the feature it serves; relocate the AppHeader function (and keep SidebarTrigger import) into the feature's components file, update any consumer imports to point to the new module, and add an export from the feature's public index so other code imports remain stable.apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx (1)
14-20: Consider using the shared Input component for consistency.The component uses a native
<input>element. For consistency with the rest of the codebase, consider using the sharedInputcomponent from@tokenization/ui/input, similar to how it's used inCreateVault.tsx.♻️ Suggested refactor
+"use client"; + +import { Search } from "lucide-react"; +import { Input } from "@tokenization/ui/input"; + +interface CampaignSearchProps { + value: string; + onChange: (value: string) => void; +} + +export function CampaignSearch({ value, onChange }: CampaignSearchProps) { + return ( + <div className="relative"> + <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted" /> + <Input + type="search" + placeholder="Buscar campañas..." + value={value} + onChange={(e) => onChange(e.target.value)} + className="pl-9" + /> + </div> + ); +}Based on learnings: "Import shared UI components from repo/ui package (button, card, etc.)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx` around lines 14 - 20, Replace the native <input> with the shared Input component from `@tokenization/ui/input` to keep UI consistent: import Input in campaigns component, then swap the element used in CampaignSearch to <Input ... /> passing the same props (type="search", placeholder="Buscar campañas...", value, onChange handler) and preserve styling/className and accessibility attributes; mirror how CreateVault.tsx uses Input to ensure the onChange signature and any icon/slot props match the shared component's API.apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts (2)
13-15: Consider adding form validation for roiPercentage.The
roiPercentagefield has no validation rules. Consider adding constraints to ensure valid input (e.g., min 0, max 100).♻️ Proposed validation
+import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +const roiFormSchema = z.object({ + roiPercentage: z.number().min(0).max(100), +}); +export type RoiFormValues = z.infer<typeof roiFormSchema>; export function useRoi() { // ... const roiForm = useForm<RoiFormValues>({ defaultValues: { roiPercentage: 0 }, + resolver: zodResolver(roiFormSchema), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts` around lines 13 - 15, The roiPercentage field in the useForm setup (useForm<RoiFormValues>) has no validation rules; update the roiForm initialization to add validation for roiPercentage (e.g., required, min: 0, max: 100 and numeric) via the form library's validation API so inputs outside 0–100 or non-numeric values are rejected; adjust any consuming code (handlers/components reading roiForm, e.g., roiForm.getValues/handleSubmit and form fields) to surface validation errors accordingly.
34-42: Placeholder implementations with console.log.The
onSubmitRoiandonFundNowhandlers currently only log to console. This is fine for UI mockups, but ensure these are connected to actual API calls before shipping to production.Would you like me to help implement the actual ROI creation and funding API integration?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts` around lines 34 - 42, Replace the placeholder console.log calls in onSubmitRoi and onFundNow with real API integration: in onSubmitRoi (the roiForm.handleSubmit handler) call the createRoi/createRoiForCampaign API passing the validated data and roiDialogCampaign?.id, await the response, handle errors (try/catch), show success/error feedback (toast or snackbar), reset roiForm and call closeRoiDialog() and any campaign refetch or state update on success; in onFundNow call the fundCampaign API with fundsDialogCampaign?.id and the amount, await response, handle errors, show feedback, update balances or campaign state, and then call closeFundsDialog(); ensure to import and use existing API helpers and trigger relevant cache invalidation or refetch after successful calls.apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx (1)
52-140: Consider handling empty campaigns state.When
campaignsis an empty array, the table renders headers with an empty body. Consider adding an empty state message for better UX, similar to howCampaignListhandles it.♻️ Proposed empty state handling
<TableBody> + {campaigns.length === 0 && ( + <TableRow> + <TableCell colSpan={5} className="text-center py-8 text-text-muted"> + No hay campañas disponibles para mostrar ROI. + </TableCell> + </TableRow> + )} {campaigns.map((campaign) => { // ... existing code })} </TableBody>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx` around lines 52 - 140, Add an empty-state row when campaigns is empty: inside the TableBody in roi-table.tsx, check if campaigns.length === 0 and render a single TableRow with one TableCell spanning all columns (use colspan equal to number of columns) showing a friendly empty message/icon and optional CTA (e.g., "No campaigns found" and a button to create one). Place this check before mapping campaigns so existing mapCampaignProgress, CAMPAIGN_STATUS_CONFIG, and action handlers (onCreateRoi, onAddFunds) remain unchanged and layout stays consistent.apps/backoffice-tokenization/src/components/shared/campaign-card.tsx (1)
1-97: Component location may not follow feature-based structure.The coding guidelines recommend organizing frontends using a feature-based folder structure in
src/features/. This component is placed insrc/components/shared/. If it's specific to campaigns, consider moving it tosrc/features/campaigns/components/. Shared components that are truly reusable across multiple features may justify remaining here.As per coding guidelines: "Organize Next.js frontends using feature-based folder structure in
src/features/"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx` around lines 1 - 97, The CampaignCard component is placed under a generic shared folder but belongs to the campaigns feature; move the CampaignCard export (function CampaignCard) and its file into the campaigns feature (e.g., features/campaigns/components/) so it lives with CAMPAIGN_STATUS_CONFIG, mapCampaignProgress and Campaign types, update any imports that reference CampaignCard to the new path, and if it truly is cross-feature keep it shared but add a clear README or index export to justify shared placement; ensure the component's named export and props (CampaignCard, CampaignCardProps, onSeeEscrow) remain unchanged so callers only need path updates.apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts (1)
3-23: Consider extracting error details from failed responses.The current error handling discards the response body which may contain useful error messages from the API. For better debugging and user feedback, consider parsing the error response.
♻️ Proposed error handling improvement
export async function getCampaigns(): Promise<Campaign[]> { const res = await fetch("/api/campaigns"); - if (!res.ok) throw new Error("Failed to fetch campaigns"); + if (!res.ok) { + const error = await res.json().catch(() => ({})); + throw new Error(error.message ?? "Failed to fetch campaigns"); + } return res.json(); } export async function getCampaignById(id: string): Promise<Campaign> { const res = await fetch(`/api/campaigns/${id}`); - if (!res.ok) throw new Error("Failed to fetch campaign"); + if (!res.ok) { + const error = await res.json().catch(() => ({})); + throw new Error(error.message ?? "Failed to fetch campaign"); + } return res.json(); } export async function createCampaign(data: CreateCampaignFormValues): Promise<Campaign> { const res = await fetch("/api/campaigns", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); - if (!res.ok) throw new Error("Failed to create campaign"); + if (!res.ok) { + const error = await res.json().catch(() => ({})); + throw new Error(error.message ?? "Failed to create campaign"); + } return res.json(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts` around lines 3 - 23, Update error handling in getCampaigns, getCampaignById, and createCampaign to include details from the response body when res.ok is false: read and parse the response body (try res.json() then fallback to res.text()), extract a meaningful message or include the parsed content, and throw a new Error that combines a descriptive prefix (e.g., "Failed to fetch campaigns", "Failed to fetch campaign", "Failed to create campaign") with the extracted error details so callers receive useful diagnostic info.apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx (1)
22-33: Consider using the sharedButtoncomponent from@tokenization/ui.The coding guidelines recommend importing shared UI components from the
@repo/uipackage. Using theButtoncomponent would ensure consistent styling and behavior across the application.♻️ Proposed refactor to use shared Button
+"use client"; + +import { Button } from "@tokenization/ui/button"; import type { CampaignStatus } from "@/features/campaigns/types/campaign.types"; // ... STATUS_OPTIONS ... export function CampaignFilter({ value, onChange }: CampaignFilterProps) { return ( <div className="flex gap-2 flex-wrap"> {STATUS_OPTIONS.map((option) => ( - <button + <Button key={option.value} - type="button" + variant={value === option.value ? "default" : "secondary"} + size="sm" onClick={() => onChange(option.value)} - className={`h-8 rounded-lg px-3 text-xs font-medium transition-colors ${ - value === option.value - ? "bg-primary text-primary-foreground" - : "bg-secondary text-secondary-foreground hover:bg-secondary/70" - }`} + className="h-8 px-3 text-xs font-medium cursor-pointer" > {option.label} - </button> + </Button> ))} </div> ); }As per coding guidelines: "Import shared UI components from
@repo/uipackage (button, card, etc.)"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx` around lines 22 - 33, Replace the raw <button> used inside the CampaignFilter component with the shared Button component from `@tokenization/ui`: add the Button import, render <Button> for each option (preserving key={option.value}, type="button" and onClick={() => onChange(option.value)}), map the active state check (value === option.value) to the Button's appropriate props or className/variant so the selected styling ("bg-primary text-primary-foreground") and unselected hover behavior remain, and ensure option.label is passed as the Button children; update any accessibility attributes as needed.apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx (1)
4-21: Use the shared UI package alias the frontend standard expects.These primitives are imported from
@tokenization/ui/*, but the app guideline standardizes shared UI imports through@repo/ui/*. Please align this file so new screens don’t introduce a second import convention.As per coding guidelines, "Import shared UI components from
@repo/uipackage (button, card, etc.)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx` around lines 4 - 21, Update the shared UI imports to use the standardized package alias: replace imports that reference "@tokenization/ui/..." with the corresponding "@repo/ui/..." modules so Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input and Button are all imported from "@repo/ui/..." (ensure you update every import statement in create-roi-dialog.tsx that uses the old "@tokenization/ui" path to the new "@repo/ui" path).apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx (1)
5-6: Use the repo path alias for sibling feature imports.These relative imports are the only ones in the file that don’t follow the app’s
@/*convention, which makes refactors more brittle.As per coding guidelines, "Use path alias `@/*` mapping to `./src/*` for imports".Proposed fix
-import { CampaignToolbar } from "./campaign-toolbar"; -import { CampaignList } from "./campaign-list"; +import { CampaignToolbar } from "@/features/campaigns/components/campaign-toolbar"; +import { CampaignList } from "@/features/campaigns/components/campaign-list";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx` around lines 5 - 6, Replace the two relative sibling imports for CampaignToolbar and CampaignList with the repo path-alias form that uses `@/`* (which maps to ./src/*); update the import statements that reference the CampaignToolbar and CampaignList components to use the alias-based path so they follow the app-wide `@/*` convention and remain stable during refactors.apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx (2)
8-10: Use the repo alias for local feature imports.These relative imports drift from the import convention used elsewhere in the app. Switching them to
@/features/...keeps moves and refactors safer.As per coding guidelines, "Use path alias `@/*` mapping to `./src/*` for imports".📦 Import cleanup
-import { StepCampaignBasics } from "./step-campaign-basics"; -import { StepEscrowConfig } from "./step-escrow-config"; -import { StepCreateToken } from "./step-create-token"; +import { StepCampaignBasics } from "@/features/campaigns/components/create/step-campaign-basics"; +import { StepEscrowConfig } from "@/features/campaigns/components/create/step-escrow-config"; +import { StepCreateToken } from "@/features/campaigns/components/create/step-create-token";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx` around lines 8 - 10, The imports in create-campaign-stepper.tsx use relative paths; update them to use the project path alias by replacing the three imports (StepCampaignBasics, StepEscrowConfig, StepCreateToken) with their alias equivalents under "@/features/campaigns/components/create/..." so they follow the repo convention (use '@/features/...' instead of relative paths) and keep imports consistent across refactors.
65-109: Prefer a real<form>around the step content and final action.The last action is wired through
onClick, so the flow misses native submit semantics like Enter-to-submit on the final step. Wrapping the content/navigation in a<form onSubmit={onSubmit}>and usingtype="submit"on the last button would make the stepper behave like a form end-to-end.📝 Minimal shape
- <div className="rounded-xl border border-border bg-card p-6"> - {step === 1 && <StepCampaignBasics form={form} />} - {step === 2 && ( - <StepEscrowConfig form={form} totalCommitment={totalCommitment} /> - )} - {step === 3 && <StepCreateToken form={form} />} - </div> - - {/* Error */} - {error && ( - <p className="text-sm text-destructive" role="alert"> - {error} - </p> - )} - - {/* Navigation */} - <div className="flex justify-between"> + <form onSubmit={onSubmit} className="flex flex-col gap-6"> + <div className="rounded-xl border border-border bg-card p-6"> + {step === 1 && <StepCampaignBasics form={form} />} + {step === 2 && ( + <StepEscrowConfig form={form} totalCommitment={totalCommitment} /> + )} + {step === 3 && <StepCreateToken form={form} />} + </div> + + {error && ( + <p className="text-sm text-destructive" role="alert"> + {error} + </p> + )} + + <div className="flex justify-between"> <Button type="button" variant="outline" onClick={step === 1 ? () => router.push("/campaigns") : prevStep} > {step === 1 ? "Cancelar" : "← Atrás"} </Button> - {step < totalSteps ? ( - <Button type="button" onClick={nextStep}> - Siguiente → - </Button> - ) : ( - <Button type="button" onClick={onSubmit} disabled={isSubmitting}> - {isSubmitting ? ( - <> - <Loader2 className="size-4 animate-spin" /> - Procesando... - </> - ) : ( - <> - <PenLine className="size-4" /> - Confirmar y Firmar - </> - )} - </Button> - )} - </div> + {step < totalSteps ? ( + <Button type="button" onClick={nextStep}> + Siguiente → + </Button> + ) : ( + <Button type="submit" disabled={isSubmitting}> + {isSubmitting ? ( + <> + <Loader2 className="size-4 animate-spin" /> + Procesando... + </> + ) : ( + <> + <PenLine className="size-4" /> + Confirmar y Firmar + </> + )} + </Button> + )} + </div> + </form>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx` around lines 65 - 109, Wrap the step content and navigation block in a real <form> with onSubmit={onSubmit} so native submit semantics (Enter key, form validation) work; keep the per-step navigation buttons as type="button" (e.g., the Cancel/Back and Next buttons) and change the final confirmation button (currently using onClick and disabled={isSubmitting}) to type="submit" so it triggers the form submit; ensure the onSubmit handler signature in this component (onSubmit) accepts the event and calls event.preventDefault() only if it needs to manage submission manually, or let the handler perform submission directly when invoked by the form. Reference components/vars: StepCampaignBasics, StepEscrowConfig, StepCreateToken, nextStep, prevStep, onSubmit, isSubmitting.apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx (1)
4-13: Align shared UI imports with the monorepo package.These form primitives are coming from
@tokenization/ui/*, but the frontend guideline asks feature code to consume shared UI from@repo/ui. Please switch this file to the shared package alias the repo standardizes on.As per coding guidelines, "Import shared UI components from
@repo/uipackage (button, card, etc.)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx` around lines 4 - 13, Update the imports for the shared form and input primitives to use the monorepo standard package alias: replace imports of Form, FormControl, FormField, FormItem, FormLabel, FormMessage from "@tokenization/ui/form" and Input and Textarea from "@tokenization/ui/input" to import those same symbols from the consolidated package "@repo/ui" (preserve the exact named imports like Form, Input, Textarea, etc. and keep usage in step-campaign-basics.tsx unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/backoffice-tokenization/src/app/`(dashboard)/campaigns/[id]/loans/page.tsx:
- Around line 13-17: The ManageLoansView is not receiving the route campaign id
so the page renders unscoped data; update the page component to pass the route
id into ManageLoansView (e.g., <ManageLoansView campaignId={id} />) and then
update ManageLoansView to accept a campaignId prop and thread that prop into any
internal hooks or functions (e.g., useManageLoans, fetchLoans, createLoan,
updateLoan) so all fetches/mutations are scoped to the provided campaignId;
ensure prop name is consistently used throughout the component and any child
hooks/components.
In `@apps/backoffice-tokenization/src/app/layout.tsx`:
- Around line 11-18: The Tailwind config is missing a fontFamily entry to map
the CSS variable set by Inter (variable: "--font-inter") to the tailwind sans
family; update your tailwind.config.ts theme.extend to add fontFamily.sans =
['var(--font-inter)'] so that the Inter font configured in the Inter(...) call
is actually applied when using the class font-sans (reference the Inter
import/const inter and the CSS variable --font-inter).
In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx`:
- Around line 21-28: Remove unused props and variables from the CampaignCard
component and its props type: from the function signature and destructuring in
CampaignCard, delete location, organization, participants (and its default), and
targetAmount; update the CampaignCardProps interface/type to remove those fields
so the component only accepts the actually used props (campaign, onSeeEscrow)
and avoid leaving unused placeholders in the destructuring of campaign (e.g.,
remove targetAmount). Ensure no other references to those removed identifiers
remain in the file.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`:
- Around line 5-11: STATUS_OPTIONS is missing the "draft" value from the
CampaignStatus union and native <button> elements should be swapped to the
shared Button component; update the STATUS_OPTIONS array (used in
campaign-filter.tsx) to include { value: "draft", label: "Borrador" } (or
appropriate label) so it matches the CampaignStatus type, and replace any native
HTML <button> usages in this component with the Button component imported from
`@repo/ui`, adjusting props (onClick, variant, size, etc.) to match the Button
API.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx`:
- Around line 19-26: The CampaignCard instances in campaigns.map are being
passed hardcoded props (location, organization, participants); update the usage
so these values come from the campaign object (e.g., campaign.location,
campaign.organization, campaign.participants) if they vary per campaign, or
remove those props from the CampaignCard call and from the CampaignCard
component API/prop types if they are constant/unused; locate the map in
campaign-list.tsx and the CampaignCard component/prop definitions to make
matching changes to either supply dynamic fields from each campaign or eliminate
the props from the component entirely.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx`:
- Around line 14-23: The search string isn't being trimmed before performing
includes checks in the useMemo that computes filtered campaigns (see the
filtered constant in campaigns-view.tsx), so padded input like " campaña " fails
matchesSearch; fix by creating a trimmedSearch = search.trim() (or reuse
trimmedSearch variable) and use trimmedSearch for the empty-check and for all
calls to toLowerCase().includes(...) when filtering MOCK_CAMPAIGNS, leaving the
rest of the matchesStatus logic unchanged.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx`:
- Around line 74-80: The durationDays input currently accepts decimals; change
the onChange to normalize to an integer before calling field.onChange by
wrapping the parsed value (from parseNumericInput) with an integer conversion
(e.g., Math.floor or Math.round) so downstream scheduling receives whole days
only; update the Input usage that references parseNumericInput and
field.onChange (and keep numericInputKeyDown) to call
field.onChange(Math.floor(parseNumericInput(e.target.value))) and ensure any
validation for durationDays expects an integer.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx`:
- Around line 57-64: The form still uses Vault/price copy: update all
occurrences tied to the roiPercentage field in create-roi-dialog.tsx — change
FormLabel text from "Precio (%)" to a proper ROI label (e.g., "ROI (%)"), update
validation messages (required/min/max) to reference ROI instead of precio, and
update the primary CTA text that currently reads "Crear Vault" to something like
"Crear ROI" or "Guardar ROI"; ensure these changes are applied to the render
block using roiPercentage and the duplicated instances around lines 115-117 as
well.
- Around line 97-102: The "Leer más →" button in create-roi-dialog.tsx is a
focusable control with no action; either remove it or wire it up: replace the
interactive <button> with non-interactive plain text (e.g., a <span> or visually
identical element) if no destination exists, or add a proper navigation/action
by turning it into a link or attaching an onClick handler that calls the
appropriate navigation function (e.g., openRoiHelp, navigateToRoiDocs) used
elsewhere in this component (or use the app's Link component) so the control is
not a dead, focusable element.
- Around line 34-39: Create a local submitting flag in CreateRoiDialog and use
it to disable the footer action buttons while the async onSubmit is in flight:
in the submit handler used by the form, set isSubmitting = true immediately
before awaiting props.onSubmit(...) and set isSubmitting = false in a finally
block so it always resets on error/success; wire this flag to disable the
primary/secondary footer buttons (and prevent the Enter key from re-triggering)
so rapid clicks/Enter won’t call onSubmit multiple times. Ensure you reference
the existing CreateRoiDialog submit handler and the footer action button props
when applying the change.
In
`@apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts`:
- Around line 51-55: In onSubmit (the form.handleSubmit callback) stop creating
a milestone when walletAddress is falsy: validate walletAddress before calling
setMilestones and, if missing, block the submission and surface a user-facing
error (e.g., call the form error API such as form.setError on a relevant field
or trigger the app's toast/error UI) so the user sees "Wallet not connected" (or
similar); only call setMilestones to append { id: Date.now().toString(),
...values, walletAddress, status: "active" } when walletAddress is present.
- Around line 41-49: saveEdit currently writes editValues into state without
validation, allowing empty description or zero amount; update saveEdit to
validate editValues before calling setMilestones (or integrate editing into the
existing react-hook-form). Specifically, in saveEdit (which uses editingId,
editValues, setMilestones, setEditingId) add a guard that checks the same
constraints used on create (e.g., non-empty description and amount > 0) and only
call setMilestones and setEditingId(null) when validation passes; if invalid,
abort and surface an error/validation state so the UI can show feedback (or
alternatively switch the edit flow to use react-hook-form so existing validators
run).
In `@apps/backoffice-tokenization/src/features/campaigns/types/campaign.types.ts`:
- Around line 1-7: Update the CampaignStatus type to align with the persisted
backend enum (use values DRAFT, ACTIVE, FUNDED, PAUSED, CLOSED) or else rename
the existing CampaignStatus to something like UI_CampaignStatus and add an
explicit mapper function between backend status and UI status; modify the
Campaign interface (Campaign.status) to reference the updated type name used,
and ensure any code referring to CampaignStatus/Campaign.status is updated to
use the new enum values or the mapper.
In `@apps/backoffice-tokenization/src/lib/numeric-input.ts`:
- Around line 3-13: The keydown handler numericInputKeyDown prevents Home and
End, blocking standard caret navigation; update the allowlist ALLOWED_KEYS to
include "Home" and "End" so those keys pass through (i.e., add "Home" and "End"
to the ALLOWED_KEYS array used by numericInputKeyDown) and ensure the existing
checks still respect ctrl/meta combos.
- Around line 16-19: parseNumericInput currently corrupts localized/grouped
numbers like "1.234,56" because it naively replaces only the first comma; update
parseNumericInput to correctly handle grouping and decimal separators by:
sanitize the input to keep only digits, dots, commas and spaces, then detect the
last occurrence of either '.' or ',' and treat that as the decimal separator
(convert it to '.'), remove all other grouping separators (dots, spaces,
non-digit chars), parse the resulting canonical "1234.56" string with Number,
and preserve the existing max cap logic; modify the implementation inside
parseNumericInput to use this approach so grouped formats (e.g., es-CO) parse
correctly.
---
Nitpick comments:
In `@apps/backoffice-tokenization/src/components/layout/app-header.tsx`:
- Around line 1-11: Move the AppHeader component out of the generic components
layout folder into the layout/dashboard feature's components module so it is
colocated with the feature it serves; relocate the AppHeader function (and keep
SidebarTrigger import) into the feature's components file, update any consumer
imports to point to the new module, and add an export from the feature's public
index so other code imports remain stable.
In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx`:
- Around line 1-97: The CampaignCard component is placed under a generic shared
folder but belongs to the campaigns feature; move the CampaignCard export
(function CampaignCard) and its file into the campaigns feature (e.g.,
features/campaigns/components/) so it lives with CAMPAIGN_STATUS_CONFIG,
mapCampaignProgress and Campaign types, update any imports that reference
CampaignCard to the new path, and if it truly is cross-feature keep it shared
but add a clear README or index export to justify shared placement; ensure the
component's named export and props (CampaignCard, CampaignCardProps,
onSeeEscrow) remain unchanged so callers only need path updates.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`:
- Around line 22-33: Replace the raw <button> used inside the CampaignFilter
component with the shared Button component from `@tokenization/ui`: add the Button
import, render <Button> for each option (preserving key={option.value},
type="button" and onClick={() => onChange(option.value)}), map the active state
check (value === option.value) to the Button's appropriate props or
className/variant so the selected styling ("bg-primary text-primary-foreground")
and unselected hover behavior remain, and ensure option.label is passed as the
Button children; update any accessibility attributes as needed.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx`:
- Around line 14-20: Replace the native <input> with the shared Input component
from `@tokenization/ui/input` to keep UI consistent: import Input in campaigns
component, then swap the element used in CampaignSearch to <Input ... /> passing
the same props (type="search", placeholder="Buscar campañas...", value, onChange
handler) and preserve styling/className and accessibility attributes; mirror how
CreateVault.tsx uses Input to ensure the onChange signature and any icon/slot
props match the shared component's API.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx`:
- Around line 5-6: Replace the two relative sibling imports for CampaignToolbar
and CampaignList with the repo path-alias form that uses `@/`* (which maps to
./src/*); update the import statements that reference the CampaignToolbar and
CampaignList components to use the alias-based path so they follow the app-wide
`@/*` convention and remain stable during refactors.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx`:
- Around line 14-22: Replace the native <button> with the shared Button
component from `@repo/ui`: import Button from '@repo/ui' (or named export used in
the codebase), then render <Button> with the same props as the current
element—pass type="button", onClick={handleClaim}, disabled={disabled}, preserve
the className or map styling to Button props, and keep the button label
"Reclamar ROI"; update any prop names to match Button's API if it differs (e.g.,
className -> className or variant/size) and remove the native button element.
- Around line 9-12: The placeholder handleClaim in the ClaimRoiButton component
should call a server-side API that builds and submits the Soroban claim ROI
transaction instead of doing it in the client; implement a new exported function
claimRoi(campaignId, walletInfo) in src/lib/sorobanClient.ts that
constructs/signs/submits the Soroban transaction and returns the submission
result, add an API route (e.g. POST /api/campaigns/:id/claim-roi) that invokes
sorobanClient.claimRoi and returns success/error, then update the component's
handleClaim to call that API, handle loading/error states and surface errors to
the user; ensure errors from sorobanClient.claimRoi are logged and returned to
the client for display.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx`:
- Around line 8-10: The imports in create-campaign-stepper.tsx use relative
paths; update them to use the project path alias by replacing the three imports
(StepCampaignBasics, StepEscrowConfig, StepCreateToken) with their alias
equivalents under "@/features/campaigns/components/create/..." so they follow
the repo convention (use '@/features/...' instead of relative paths) and keep
imports consistent across refactors.
- Around line 65-109: Wrap the step content and navigation block in a real
<form> with onSubmit={onSubmit} so native submit semantics (Enter key, form
validation) work; keep the per-step navigation buttons as type="button" (e.g.,
the Cancel/Back and Next buttons) and change the final confirmation button
(currently using onClick and disabled={isSubmitting}) to type="submit" so it
triggers the form submit; ensure the onSubmit handler signature in this
component (onSubmit) accepts the event and calls event.preventDefault() only if
it needs to manage submission manually, or let the handler perform submission
directly when invoked by the form. Reference components/vars:
StepCampaignBasics, StepEscrowConfig, StepCreateToken, nextStep, prevStep,
onSubmit, isSubmitting.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx`:
- Around line 4-13: Update the imports for the shared form and input primitives
to use the monorepo standard package alias: replace imports of Form,
FormControl, FormField, FormItem, FormLabel, FormMessage from
"@tokenization/ui/form" and Input and Textarea from "@tokenization/ui/input" to
import those same symbols from the consolidated package "@repo/ui" (preserve the
exact named imports like Form, Input, Textarea, etc. and keep usage in
step-campaign-basics.tsx unchanged).
In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx`:
- Around line 4-21: Update the shared UI imports to use the standardized package
alias: replace imports that reference "@tokenization/ui/..." with the
corresponding "@repo/ui/..." modules so Dialog, DialogContent,
DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl,
FormField, FormItem, FormLabel, FormMessage, Input and Button are all imported
from "@repo/ui/..." (ensure you update every import statement in
create-roi-dialog.tsx that uses the old "@tokenization/ui" path to the new
"@repo/ui" path).
In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx`:
- Around line 52-140: Add an empty-state row when campaigns is empty: inside the
TableBody in roi-table.tsx, check if campaigns.length === 0 and render a single
TableRow with one TableCell spanning all columns (use colspan equal to number of
columns) showing a friendly empty message/icon and optional CTA (e.g., "No
campaigns found" and a button to create one). Place this check before mapping
campaigns so existing mapCampaignProgress, CAMPAIGN_STATUS_CONFIG, and action
handlers (onCreateRoi, onAddFunds) remain unchanged and layout stays consistent.
In `@apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts`:
- Around line 13-15: The roiPercentage field in the useForm setup
(useForm<RoiFormValues>) has no validation rules; update the roiForm
initialization to add validation for roiPercentage (e.g., required, min: 0, max:
100 and numeric) via the form library's validation API so inputs outside 0–100
or non-numeric values are rejected; adjust any consuming code
(handlers/components reading roiForm, e.g., roiForm.getValues/handleSubmit and
form fields) to surface validation errors accordingly.
- Around line 34-42: Replace the placeholder console.log calls in onSubmitRoi
and onFundNow with real API integration: in onSubmitRoi (the
roiForm.handleSubmit handler) call the createRoi/createRoiForCampaign API
passing the validated data and roiDialogCampaign?.id, await the response, handle
errors (try/catch), show success/error feedback (toast or snackbar), reset
roiForm and call closeRoiDialog() and any campaign refetch or state update on
success; in onFundNow call the fundCampaign API with fundsDialogCampaign?.id and
the amount, await response, handle errors, show feedback, update balances or
campaign state, and then call closeFundsDialog(); ensure to import and use
existing API helpers and trigger relevant cache invalidation or refetch after
successful calls.
In
`@apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts`:
- Around line 3-23: Update error handling in getCampaigns, getCampaignById, and
createCampaign to include details from the response body when res.ok is false:
read and parse the response body (try res.json() then fallback to res.text()),
extract a meaningful message or include the parsed content, and throw a new
Error that combines a descriptive prefix (e.g., "Failed to fetch campaigns",
"Failed to fetch campaign", "Failed to create campaign") with the extracted
error details so callers receive useful diagnostic info.
In
`@apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts`:
- Around line 3-5: mapCampaignProgress currently only caps the computed percent
at 100 but can return negative or NaN for bad data; update the function
(mapCampaignProgress) to treat non-positive targetAmount as zero (return 0) and
clamp the computed value between 0 and 100 (use Math.min/Math.max or an
equivalent clamp on (campaign.raisedAmount / campaign.targetAmount) * 100) so
the returned progress is always within [0,100]; reference campaign.targetAmount
and campaign.raisedAmount when implementing the checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1d0dc375-76d3-4425-82e4-aa2eadeb6c39
📒 Files selected for processing (43)
apps/backoffice-tokenization/src/app/(dashboard)/campaigns/[id]/loans/page.tsxapps/backoffice-tokenization/src/app/(dashboard)/campaigns/loans/page.tsxapps/backoffice-tokenization/src/app/(dashboard)/campaigns/new/page.tsxapps/backoffice-tokenization/src/app/(dashboard)/campaigns/page.tsxapps/backoffice-tokenization/src/app/(dashboard)/layout.tsxapps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsxapps/backoffice-tokenization/src/app/layout.tsxapps/backoffice-tokenization/src/app/manage-escrows/page.tsxapps/backoffice-tokenization/src/app/page.tsxapps/backoffice-tokenization/src/components/layout/app-header.tsxapps/backoffice-tokenization/src/components/layout/app-sidebar.tsxapps/backoffice-tokenization/src/components/layout/page-shell.tsxapps/backoffice-tokenization/src/components/shared/campaign-card.tsxapps/backoffice-tokenization/src/components/shared/section-title.tsxapps/backoffice-tokenization/src/components/shared/stat-item.tsxapps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsxapps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsxapps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsxapps/backoffice-tokenization/src/features/campaigns/components/campaign-toolbar.tsxapps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsxapps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsxapps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsxapps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsxapps/backoffice-tokenization/src/features/campaigns/components/create/step-create-token.tsxapps/backoffice-tokenization/src/features/campaigns/components/create/step-escrow-config.tsxapps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsxapps/backoffice-tokenization/src/features/campaigns/components/roi/add-funds-dialog.tsxapps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsxapps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsxapps/backoffice-tokenization/src/features/campaigns/components/roi/roi-view.tsxapps/backoffice-tokenization/src/features/campaigns/constants/campaign-status.tsapps/backoffice-tokenization/src/features/campaigns/hooks/use-campaigns.tsapps/backoffice-tokenization/src/features/campaigns/hooks/use-create-campaign.tsapps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.tsapps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.tsapps/backoffice-tokenization/src/features/campaigns/mock/campaigns.mock.tsapps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.tsapps/backoffice-tokenization/src/features/campaigns/types/campaign.types.tsapps/backoffice-tokenization/src/features/campaigns/types/milestone.types.tsapps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.tsapps/backoffice-tokenization/src/features/vaults/deploy/dialog/CreateVault.tsxapps/backoffice-tokenization/src/lib/numeric-input.tsapps/backoffice-tokenization/src/lib/utils.ts
💤 Files with no reviewable changes (1)
- apps/backoffice-tokenization/src/app/manage-escrows/page.tsx
| <SectionTitle | ||
| title="Manejar Préstamos" | ||
| description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`} | ||
| /> | ||
| <ManageLoansView /> |
There was a problem hiding this comment.
Thread the route id into the loans view.
Right now this route is campaign-scoped only in the heading; ManageLoansView gets no id, so every /campaigns/[id]/loans page will render the same unscoped state. Please pass the campaign id into the view/hook before wiring real fetches or mutations.
🔧 Minimal shape
- <ManageLoansView />
+ <ManageLoansView campaignId={id} />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <SectionTitle | |
| title="Manejar Préstamos" | |
| description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`} | |
| /> | |
| <ManageLoansView /> | |
| <SectionTitle | |
| title="Manejar Préstamos" | |
| description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`} | |
| /> | |
| <ManageLoansView campaignId={id} /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/backoffice-tokenization/src/app/`(dashboard)/campaigns/[id]/loans/page.tsx
around lines 13 - 17, The ManageLoansView is not receiving the route campaign id
so the page renders unscoped data; update the page component to pass the route
id into ManageLoansView (e.g., <ManageLoansView campaignId={id} />) and then
update ManageLoansView to accept a campaignId prop and thread that prop into any
internal hooks or functions (e.g., useManageLoans, fetchLoans, createLoan,
updateLoan) so all fetches/mutations are scoped to the provided campaignId;
ensure prop name is consistently used throughout the component and any child
hooks/components.
| import { Inter } from "next/font/google"; | ||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| const Exo2 = localFont({ | ||
| src: "./fonts/Exo2.ttf", | ||
| variable: "---exo-2", | ||
| weight: "100 900", | ||
| display: "swap", | ||
| }); | ||
|
|
||
| const spaceGrotesk = Space_Grotesk({ | ||
| const inter = Inter({ | ||
| subsets: ["latin"], | ||
| display: "swap", | ||
| variable: "--font-inter", | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify Tailwind config has fontFamily.sans configured with --font-inter variable
# Search for tailwind config files and check fontFamily configuration
fd -e js -e ts -e mjs 'tailwind.config' apps/backoffice-tokenization --exec cat {} \; 2>/dev/null || \
fd -e js -e ts -e mjs 'tailwind.config' --max-depth 2 --exec cat {} \;Repository: Trustless-Work/tokenization-private-credit
Length of output: 454
Add fontFamily configuration to Tailwind config to apply Inter font.
The Inter font is configured with CSS variable --font-inter, but your tailwind.config.ts is missing the fontFamily.sans configuration needed for font-sans class to actually use Inter. Update the theme.extend section:
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
},
},
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backoffice-tokenization/src/app/layout.tsx` around lines 11 - 18, The
Tailwind config is missing a fontFamily entry to map the CSS variable set by
Inter (variable: "--font-inter") to the tailwind sans family; update your
tailwind.config.ts theme.extend to add fontFamily.sans = ['var(--font-inter)']
so that the Inter font configured in the Inter(...) call is actually applied
when using the class font-sans (reference the Inter import/const inter and the
CSS variable --font-inter).
| export function CampaignCard({ | ||
| campaign, | ||
| location, | ||
| organization, | ||
| participants = 0, | ||
| onSeeEscrow, | ||
| }: CampaignCardProps) { | ||
| const { title, description, status, targetAmount, raisedAmount, id } = campaign; |
There was a problem hiding this comment.
Unused props and variables.
The following are destructured but never used in the rendered output:
targetAmount(Line 28)location(Line 23)organization(Line 24)participants(Line 25, has default value but unused)
If these are placeholders for future implementation, consider removing them until needed to avoid confusion.
🧹 Proposed cleanup
export function CampaignCard({
campaign,
- location,
- organization,
- participants = 0,
onSeeEscrow,
}: CampaignCardProps) {
- const { title, description, status, targetAmount, raisedAmount, id } = campaign;
+ const { title, description, status, raisedAmount, id } = campaign;Also update the interface if these props are not needed:
interface CampaignCardProps {
campaign: Campaign;
- location?: string;
- organization?: string;
- participants?: number;
onSeeEscrow?: () => void;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx` around
lines 21 - 28, Remove unused props and variables from the CampaignCard component
and its props type: from the function signature and destructuring in
CampaignCard, delete location, organization, participants (and its default), and
targetAmount; update the CampaignCardProps interface/type to remove those fields
so the component only accepts the actually used props (campaign, onSeeEscrow)
and avoid leaving unused placeholders in the destructuring of campaign (e.g.,
remove targetAmount). Ensure no other references to those removed identifiers
remain in the file.
| const STATUS_OPTIONS: { value: CampaignStatus | "all"; label: string }[] = [ | ||
| { value: "all", label: "Todas" }, | ||
| { value: "active", label: "Activas" }, | ||
| { value: "pending", label: "Pendientes" }, | ||
| { value: "completed", label: "Completadas" }, | ||
| { value: "cancelled", label: "Canceladas" }, | ||
| ]; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the CampaignStatus type definition
fd "campaign.types.ts" --type f --exec cat {}Repository: Trustless-Work/tokenization-private-credit
Length of output: 956
🏁 Script executed:
#!/bin/bash
# Find all campaign-related type files
fd "campaign" --type f --extension "ts" --extension "tsx" | grep -E "(type|interface)" | head -20
# Also check the directory structure
fd "campaign" --type d | head -10Repository: Trustless-Work/tokenization-private-credit
Length of output: 483
🏁 Script executed:
#!/bin/bash
# Check the exact file that defines CampaignStatus used by campaign-filter.tsx
rg "export type CampaignStatus" --type ts -A 1Repository: Trustless-Work/tokenization-private-credit
Length of output: 528
🏁 Script executed:
#!/bin/bash
# Read the complete campaign-filter.tsx file to understand the intent
cat apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsxRepository: Trustless-Work/tokenization-private-credit
Length of output: 1233
The STATUS_OPTIONS array is missing the "draft" status defined in the CampaignStatus type.
The CampaignStatus type includes "draft", but the filter doesn't offer it as an option, creating an inconsistency between the type definition and available UI options. Either add "draft" to STATUS_OPTIONS or update the type definition if draft campaigns should not be filterable.
Additionally, replace the native <button> elements with the Button component from @repo/ui to align with shared component guidelines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`
around lines 5 - 11, STATUS_OPTIONS is missing the "draft" value from the
CampaignStatus union and native <button> elements should be swapped to the
shared Button component; update the STATUS_OPTIONS array (used in
campaign-filter.tsx) to include { value: "draft", label: "Borrador" } (or
appropriate label) so it matches the CampaignStatus type, and replace any native
HTML <button> usages in this component with the Button component imported from
`@repo/ui`, adjusting props (onClick, variant, size, etc.) to match the Button
API.
| {campaigns.map((campaign) => ( | ||
| <CampaignCard | ||
| key={campaign.id} | ||
| campaign={campaign} | ||
| location="Colombia" | ||
| organization="Give Colombia" | ||
| participants={12} | ||
| /> |
There was a problem hiding this comment.
Hardcoded values passed to CampaignCard.
The location, organization, and participants props are hardcoded to static values. If these should vary per campaign, they should come from the campaign data. If they're truly constant across all campaigns, consider removing them from the component API entirely (they're also unused in CampaignCard).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx`
around lines 19 - 26, The CampaignCard instances in campaigns.map are being
passed hardcoded props (location, organization, participants); update the usage
so these values come from the campaign object (e.g., campaign.location,
campaign.organization, campaign.participants) if they vary per campaign, or
remove those props from the CampaignCard call and from the CampaignCard
component API/prop types if they are constant/unused; locate the map in
campaign-list.tsx and the CampaignCard component/prop definitions to make
matching changes to either supply dynamic fields from each campaign or eliminate
the props from the component entirely.
| const saveEdit = () => { | ||
| if (!editingId) return; | ||
| setMilestones((prev) => | ||
| prev.map((m) => | ||
| m.id === editingId ? { ...m, ...editValues } : m | ||
| ) | ||
| ); | ||
| setEditingId(null); | ||
| }; |
There was a problem hiding this comment.
saveEdit can persist invalid milestone data.
The edit path bypasses the validation used on create, so an empty description or zero amount is currently saved as-is. Please validate editValues before updating state, or move edit mode onto react-hook-form too.
🧩 Minimal guard
const saveEdit = () => {
if (!editingId) return;
+ const description = editValues.description.trim();
+ if (!description || editValues.amount <= 0) return;
+
setMilestones((prev) =>
prev.map((m) =>
- m.id === editingId ? { ...m, ...editValues } : m
+ m.id === editingId ? { ...m, description, amount: editValues.amount } : m
)
);
setEditingId(null);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts`
around lines 41 - 49, saveEdit currently writes editValues into state without
validation, allowing empty description or zero amount; update saveEdit to
validate editValues before calling setMilestones (or integrate editing into the
existing react-hook-form). Specifically, in saveEdit (which uses editingId,
editValues, setMilestones, setEditingId) add a guard that checks the same
constraints used on create (e.g., non-empty description and amount > 0) and only
call setMilestones and setEditingId(null) when validation passes; if invalid,
abort and surface an error/validation state so the UI can show feedback (or
alternatively switch the edit flow to use react-hook-form so existing validators
run).
| const onSubmit = form.handleSubmit((values) => { | ||
| setMilestones((prev) => [ | ||
| ...prev, | ||
| { id: Date.now().toString(), ...values, walletAddress: walletAddress ?? "", status: "active" }, | ||
| ]); |
There was a problem hiding this comment.
Don't create milestones with an empty wallet address.
When walletAddress is missing, this still appends a new milestone with walletAddress: "". That leaves local state in a condition the UI already labels as disconnected. Please block creation until a wallet is connected and surface that reason to the user.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts`
around lines 51 - 55, In onSubmit (the form.handleSubmit callback) stop creating
a milestone when walletAddress is falsy: validate walletAddress before calling
setMilestones and, if missing, block the submission and surface a user-facing
error (e.g., call the form error API such as form.setError on a relevant field
or trigger the app's toast/error UI) so the user sees "Wallet not connected" (or
similar); only call setMilestones to append { id: Date.now().toString(),
...values, walletAddress, status: "active" } when walletAddress is present.
| export type CampaignStatus = "active" | "completed" | "pending" | "draft" | "cancelled"; | ||
|
|
||
| export interface Campaign { | ||
| id: string; | ||
| title: string; | ||
| description: string; | ||
| status: CampaignStatus; |
There was a problem hiding this comment.
Align CampaignStatus with the persisted campaign enum.
These literals do not match the status set used by the core model. As soon as this type is wired to real API data, you'll need ad-hoc remapping or the UI will reject valid backend states. Either mirror the backend enum here, or rename this to a separate UI status type and add an explicit mapper between the two.
Based on learnings: Campaign model should have status enum with values: DRAFT, ACTIVE, FUNDED, PAUSED, CLOSED.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backoffice-tokenization/src/features/campaigns/types/campaign.types.ts`
around lines 1 - 7, Update the CampaignStatus type to align with the persisted
backend enum (use values DRAFT, ACTIVE, FUNDED, PAUSED, CLOSED) or else rename
the existing CampaignStatus to something like UI_CampaignStatus and add an
explicit mapper function between backend status and UI status; modify the
Campaign interface (Campaign.status) to reference the updated type name used,
and ensure any code referring to CampaignStatus/Campaign.status is updated to
use the new enum values or the mapper.
| const ALLOWED_KEYS = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"]; | ||
|
|
||
| export function numericInputKeyDown(e: KeyboardEvent<HTMLInputElement>) { | ||
| if ( | ||
| !/[0-9.,]/.test(e.key) && | ||
| !ALLOWED_KEYS.includes(e.key) && | ||
| !e.ctrlKey && | ||
| !e.metaKey | ||
| ) { | ||
| e.preventDefault(); | ||
| } |
There was a problem hiding this comment.
Allow standard caret navigation keys.
Home and End are blocked here, so keyboard users cannot jump to the start/end of the field. They should be part of the allowlist.
Suggested change
-const ALLOWED_KEYS = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"];
+const ALLOWED_KEYS = [
+ "Backspace",
+ "Delete",
+ "ArrowLeft",
+ "ArrowRight",
+ "Home",
+ "End",
+ "Tab",
+];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ALLOWED_KEYS = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"]; | |
| export function numericInputKeyDown(e: KeyboardEvent<HTMLInputElement>) { | |
| if ( | |
| !/[0-9.,]/.test(e.key) && | |
| !ALLOWED_KEYS.includes(e.key) && | |
| !e.ctrlKey && | |
| !e.metaKey | |
| ) { | |
| e.preventDefault(); | |
| } | |
| const ALLOWED_KEYS = [ | |
| "Backspace", | |
| "Delete", | |
| "ArrowLeft", | |
| "ArrowRight", | |
| "Home", | |
| "End", | |
| "Tab", | |
| ]; | |
| export function numericInputKeyDown(e: KeyboardEvent<HTMLInputElement>) { | |
| if ( | |
| !/[0-9.,]/.test(e.key) && | |
| !ALLOWED_KEYS.includes(e.key) && | |
| !e.ctrlKey && | |
| !e.metaKey | |
| ) { | |
| e.preventDefault(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backoffice-tokenization/src/lib/numeric-input.ts` around lines 3 - 13,
The keydown handler numericInputKeyDown prevents Home and End, blocking standard
caret navigation; update the allowlist ALLOWED_KEYS to include "Home" and "End"
so those keys pass through (i.e., add "Home" and "End" to the ALLOWED_KEYS array
used by numericInputKeyDown) and ensure the existing checks still respect
ctrl/meta combos.
| export function parseNumericInput(value: string, max?: number): number { | ||
| const num = Number(value.replace(/[^0-9.,]/g, "").replace(",", ".")); | ||
| const safe = isNaN(num) ? 0 : num; | ||
| return max !== undefined ? Math.min(safe, max) : safe; |
There was a problem hiding this comment.
Localized/grouped numbers currently parse back to 0.
This parser only replaces the first comma, so any value containing both grouping and decimal separators becomes invalid. For example, 1.234,56 turns into 1.234.56, Number(...) returns NaN, and the helper falls back to 0. That is especially easy to hit here because formatCurrency() in this PR emits es-CO-formatted values.
Suggested change
export function parseNumericInput(value: string, max?: number): number {
- const num = Number(value.replace(/[^0-9.,]/g, "").replace(",", "."));
+ const sanitized = value.replace(/[^0-9.,]/g, "");
+ const lastSeparator = Math.max(
+ sanitized.lastIndexOf(","),
+ sanitized.lastIndexOf("."),
+ );
+ const normalized =
+ lastSeparator === -1
+ ? sanitized
+ : `${sanitized.slice(0, lastSeparator).replace(/[.,]/g, "")}.${sanitized
+ .slice(lastSeparator + 1)
+ .replace(/[.,]/g, "")}`;
+ const num = Number(normalized);
const safe = isNaN(num) ? 0 : num;
return max !== undefined ? Math.min(safe, max) : safe;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function parseNumericInput(value: string, max?: number): number { | |
| const num = Number(value.replace(/[^0-9.,]/g, "").replace(",", ".")); | |
| const safe = isNaN(num) ? 0 : num; | |
| return max !== undefined ? Math.min(safe, max) : safe; | |
| export function parseNumericInput(value: string, max?: number): number { | |
| const sanitized = value.replace(/[^0-9.,]/g, ""); | |
| const lastSeparator = Math.max( | |
| sanitized.lastIndexOf(","), | |
| sanitized.lastIndexOf("."), | |
| ); | |
| const normalized = | |
| lastSeparator === -1 | |
| ? sanitized | |
| : `${sanitized.slice(0, lastSeparator).replace(/[.,]/g, "")}.${sanitized | |
| .slice(lastSeparator + 1) | |
| .replace(/[.,]/g, "")}`; | |
| const num = Number(normalized); | |
| const safe = isNaN(num) ? 0 : num; | |
| return max !== undefined ? Math.min(safe, max) : safe; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backoffice-tokenization/src/lib/numeric-input.ts` around lines 16 - 19,
parseNumericInput currently corrupts localized/grouped numbers like "1.234,56"
because it naively replaces only the first comma; update parseNumericInput to
correctly handle grouping and decimal separators by: sanitize the input to keep
only digits, dots, commas and spaces, then detect the last occurrence of either
'.' or ',' and treat that as the decimal separator (convert it to '.'), remove
all other grouping separators (dots, spaces, non-digit chars), parse the
resulting canonical "1234.56" string with Number, and preserve the existing max
cap logic; modify the implementation inside parseNumericInput to use this
approach so grouped formats (e.g., es-CO) parse correctly.
Summary by CodeRabbit
Release Notes