Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SectionTitle } from "@/components/shared/section-title";
import { ManageLoansView } from "@/features/campaigns/components/loans/manage-loans-view";

interface Props {
params: Promise<{ id: string }>;
}

export default async function CampaignLoansPage({ params }: Props) {
const { id } = await params;

return (
<div className="flex flex-col gap-6">
<SectionTitle
title="Manejar Préstamos"
description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`}
/>
<ManageLoansView />
Comment on lines +13 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
<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.

</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SectionTitle } from "@/components/shared/section-title";
import { ManageLoansView } from "@/features/campaigns/components/loans/manage-loans-view";

export default function ManageLoansPage() {
return (
<div className="flex flex-col gap-6">
<SectionTitle
title="Gestionar Préstamos"
description="Administra los hitos y préstamos activos de la campaña."
/>
<ManageLoansView />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SectionTitle } from "@/components/shared/section-title";
import { CreateCampaignStepper } from "@/features/campaigns/components/create/create-campaign-stepper";

export default function NewCampaignPage() {
return (
<div className="flex flex-col gap-6">
<SectionTitle
title="Nueva Campaña"
description="Completa los pasos para configurar tu programa de micro-préstamos."
/>
<CreateCampaignStepper />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Link from "next/link";
import { SectionTitle } from "@/components/shared/section-title";
import { CampaignsView } from "@/features/campaigns/components/campaigns-view";
import { Button } from "@tokenization/ui/button";
import { Plus } from "lucide-react";

export default function CampaignsPage() {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap justify-between gap-1">
<SectionTitle
title="Campañas"
description="Consulta y gestiona las campañas de inversión activas."
/>
<div className="flex gap-2">

<Button size="lg" asChild>
<Link href="/campaigns/new">
<Plus size={16} />
Nueva Campaña
</Link>
</Button>
</div>
</div>
<CampaignsView />
</div>
);
}
6 changes: 6 additions & 0 deletions apps/backoffice-tokenization/src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ReactNode } from "react";
import { PageShell } from "@/components/layout/page-shell";

export default function DashboardLayout({ children }: { children: ReactNode }) {
return <PageShell>{children}</PageShell>;
}
14 changes: 14 additions & 0 deletions apps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SectionTitle } from "@/components/shared/section-title";
import { RoiView } from "@/features/campaigns/components/roi/roi-view";

export default function RoiPage() {
return (
<div className="flex flex-col gap-6">
<SectionTitle
title="Retornos"
description="Gestione y monitoree sus programas de ROI activos en tiempo real."
/>
<RoiView />
</div>
);
}
34 changes: 5 additions & 29 deletions apps/backoffice-tokenization/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ReactQueryClientProvider } from "@tokenization/tw-blocks-shared/src/providers/ReactQueryClientProvider";
import { TrustlessWorkProvider } from "@tokenization/tw-blocks-shared/src/providers/TrustlessWork";
Expand All @@ -9,21 +8,13 @@ import { EscrowAmountProvider } from "@tokenization/tw-blocks-shared/src/provide
import { Toaster } from "@tokenization/ui/sonner";
import { WalletProvider } from "@tokenization/tw-blocks-shared/src/wallet-kit/WalletProvider";
import type { ReactNode } from "react";
import { Header } from "@/components/shared/Header";
import { Space_Grotesk } from "next/font/google";
import localFont from "next/font/local";
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",
});
Comment on lines +11 to 18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 const metadata: Metadata = {
Expand All @@ -38,29 +29,14 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={cn(
Exo2.variable,
"antialiased dark",
spaceGrotesk.className
)}
>
<body className={cn(inter.variable, "antialiased font-sans")}>
<ReactQueryClientProvider>
<TrustlessWorkProvider>
<WalletProvider>
<EscrowProvider>
<EscrowDialogsProvider>
<EscrowAmountProvider>
<div className="relative flex min-h-screen w-full">
<div className="flex-1 flex flex-col w-full">
<div className="container mx-auto">
<Header />

{children}
</div>
</div>
</div>

{children}
<Toaster position="top-right" richColors />
</EscrowAmountProvider>
</EscrowDialogsProvider>
Expand Down
12 changes: 0 additions & 12 deletions apps/backoffice-tokenization/src/app/manage-escrows/page.tsx

This file was deleted.

8 changes: 7 additions & 1 deletion apps/backoffice-tokenization/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { HomeView } from "@/features/home/HomeView";
import { Header } from "@/components/shared/Header";

export default function Home() {
return <HomeView />;
return (
<div className="container mx-auto">
<Header />
<HomeView />
</div>
);
}
11 changes: 11 additions & 0 deletions apps/backoffice-tokenization/src/components/layout/app-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { SidebarTrigger } from "@tokenization/ui/sidebar";

export function AppHeader() {
return (
<header className="flex h-14 shrink-0 items-center border-b border-border bg-card px-4 md:hidden">
<SidebarTrigger />
</header>
);
}
47 changes: 47 additions & 0 deletions apps/backoffice-tokenization/src/components/layout/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import Image from "next/image";
import { TrendingUp, Wallet } from "lucide-react";
import {
AppSidebar as SharedAppSidebar,
type AppSidebarNavItem,
type AppSidebarLogoConfig,
} from "@tokenization/ui/app-sidebar";
import { SidebarWalletButton } from "@tokenization/ui/sidebar-wallet-button";

const logo: AppSidebarLogoConfig = {
element: (
<Image
src="/interactuar_logo.png"
alt="interactuar"
width={160}
height={32}
priority
style={{ objectFit: "contain" }}
/>
),
href: "/",
};

const navItems: AppSidebarNavItem[] = [
{
label: "Campañas",
href: "/campaigns",
icon: Wallet,
},
{
label: "Retorno de Inversión",
href: "/roi",
icon: TrendingUp,
},
];

export function AppSidebar() {
return (
<SharedAppSidebar
navItems={navItems}
logo={logo}
footerContent={<SidebarWalletButton />}
/>
);
}
20 changes: 20 additions & 0 deletions apps/backoffice-tokenization/src/components/layout/page-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ReactNode } from "react";
import { AppSidebar } from "@/components/layout/app-sidebar";
import { AppHeader } from "@/components/layout/app-header";
import { SidebarInset, SidebarProvider } from "@tokenization/ui/sidebar";

interface PageShellProps {
children: ReactNode;
}

export function PageShell({ children }: PageShellProps) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<AppHeader />
<div className="flex flex-1 flex-col gap-4 p-6">{children}</div>
</SidebarInset>
</SidebarProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";

import Link from "next/link";
import { Badge } from "@tokenization/ui/badge";
import { Button } from "@tokenization/ui/button";
import { Progress } from "@tokenization/ui/progress";
import { cn } from "@tokenization/shared/lib/utils";
import { ExternalLink, Landmark } from "lucide-react";
import type { Campaign } from "@/features/campaigns/types/campaign.types";
import { CAMPAIGN_STATUS_CONFIG } from "@/features/campaigns/constants/campaign-status";
import { mapCampaignProgress } from "@/features/campaigns/utils/campaign.mapper";

interface CampaignCardProps {
campaign: Campaign;
location?: string;
organization?: string;
participants?: number;
onSeeEscrow?: () => void;
}

export function CampaignCard({
campaign,
location,
organization,
participants = 0,
onSeeEscrow,
}: CampaignCardProps) {
const { title, description, status, targetAmount, raisedAmount, id } = campaign;
Comment on lines +21 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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 progress = mapCampaignProgress(campaign);

const statusCfg = CAMPAIGN_STATUS_CONFIG[status];

return (
<div
className={cn(
"flex flex-col gap-4 rounded-xl border border-border bg-card p-5",
"shadow-card hover:shadow-hover",
"transition-shadow duration-200"
)}
>
{/* Top row: status + action */}
<div className="flex items-center justify-between">
<Badge
variant="outline"
className={cn("text-xs font-semibold uppercase tracking-wide", statusCfg.className)}
>
{statusCfg.label}
</Badge>

<Button size="sm" className="cursor-pointer gap-1.5" asChild>
<Link href={`/campaigns/${id}/loans`}>
<Landmark className="size-3.5" />
Manejar Préstamos
</Link>
</Button>
</div>

{/* Title & subtitle */}
<div className="flex flex-col gap-0.5">
<h3 className="text-lg font-bold text-foreground leading-tight">
#{id.slice(0, 3).toUpperCase()} {title}
</h3>
</div>

{/* Description */}
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2">
{description}
</p>

{/* Bottom row: participants + escrow link | progress */}
<div className="flex items-end justify-between gap-4 pt-1">
{/* Participants + escrow */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
onClick={onSeeEscrow}
className="flex items-center gap-1 text-xs font-semibold text-primary hover:text-primary/80 transition-colors cursor-pointer"
>
See Escrow
<ExternalLink className="size-3" />
</Button>
</div>

{/* Progress */}
<div className="flex flex-col items-end gap-1.5 min-w-40">
<div className="flex items-center justify-between w-full">
<span className="text-[10px] font-semibold uppercase tracking-widest text-text-muted">
Loans Completed
</span>
<span className="text-xs font-bold text-foreground">{progress}%</span>
</div>
<Progress value={progress} className="h-1.5 w-full" />
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface SectionTitleProps {
title: string;
description?: string;
}

export function SectionTitle({ title, description }: SectionTitleProps) {
return (
<div className="flex flex-col gap-1">
<h2 className="text-3xl font-bold text-foreground">{title}</h2>
{description && (
<p className="text-md text-text-secondary">{description}</p>
)}
</div>
);
}
Loading
Loading