diff --git a/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/[id]/loans/page.tsx b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/[id]/loans/page.tsx new file mode 100644 index 0000000..6136f37 --- /dev/null +++ b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/[id]/loans/page.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/loans/page.tsx b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/loans/page.tsx new file mode 100644 index 0000000..3a478cc --- /dev/null +++ b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/loans/page.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/new/page.tsx b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/new/page.tsx new file mode 100644 index 0000000..218475a --- /dev/null +++ b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/new/page.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/page.tsx b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/page.tsx new file mode 100644 index 0000000..3f5ea67 --- /dev/null +++ b/apps/backoffice-tokenization/src/app/(dashboard)/campaigns/page.tsx @@ -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 ( +
+
+ +
+ + +
+
+ +
+ ); +} diff --git a/apps/backoffice-tokenization/src/app/(dashboard)/layout.tsx b/apps/backoffice-tokenization/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..b2d6f6a --- /dev/null +++ b/apps/backoffice-tokenization/src/app/(dashboard)/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactNode } from "react"; +import { PageShell } from "@/components/layout/page-shell"; + +export default function DashboardLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/apps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsx b/apps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsx new file mode 100644 index 0000000..eaa771e --- /dev/null +++ b/apps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/backoffice-tokenization/src/app/layout.tsx b/apps/backoffice-tokenization/src/app/layout.tsx index b882373..59f9736 100644 --- a/apps/backoffice-tokenization/src/app/layout.tsx +++ b/apps/backoffice-tokenization/src/app/layout.tsx @@ -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"; @@ -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", }); export const metadata: Metadata = { @@ -38,29 +29,14 @@ export default function RootLayout({ }>) { return ( - + -
-
-
-
- - {children} -
-
-
- + {children}
diff --git a/apps/backoffice-tokenization/src/app/manage-escrows/page.tsx b/apps/backoffice-tokenization/src/app/manage-escrows/page.tsx deleted file mode 100644 index 602c608..0000000 --- a/apps/backoffice-tokenization/src/app/manage-escrows/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import { Suspense } from "react"; -import { ManageEscrowsView } from "@/features/manage-escrows/ManageEscrowsView"; - -export default function ManageEscrows() { - return ( - - - - ); -} diff --git a/apps/backoffice-tokenization/src/app/page.tsx b/apps/backoffice-tokenization/src/app/page.tsx index cb41f62..3c39149 100644 --- a/apps/backoffice-tokenization/src/app/page.tsx +++ b/apps/backoffice-tokenization/src/app/page.tsx @@ -1,5 +1,11 @@ import { HomeView } from "@/features/home/HomeView"; +import { Header } from "@/components/shared/Header"; export default function Home() { - return ; + return ( +
+
+ +
+ ); } diff --git a/apps/backoffice-tokenization/src/components/layout/app-header.tsx b/apps/backoffice-tokenization/src/components/layout/app-header.tsx new file mode 100644 index 0000000..e744662 --- /dev/null +++ b/apps/backoffice-tokenization/src/components/layout/app-header.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { SidebarTrigger } from "@tokenization/ui/sidebar"; + +export function AppHeader() { + return ( +
+ +
+ ); +} diff --git a/apps/backoffice-tokenization/src/components/layout/app-sidebar.tsx b/apps/backoffice-tokenization/src/components/layout/app-sidebar.tsx new file mode 100644 index 0000000..1baf5e8 --- /dev/null +++ b/apps/backoffice-tokenization/src/components/layout/app-sidebar.tsx @@ -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: ( + interactuar + ), + href: "/", +}; + +const navItems: AppSidebarNavItem[] = [ + { + label: "Campañas", + href: "/campaigns", + icon: Wallet, + }, + { + label: "Retorno de Inversión", + href: "/roi", + icon: TrendingUp, + }, +]; + +export function AppSidebar() { + return ( + } + /> + ); +} diff --git a/apps/backoffice-tokenization/src/components/layout/page-shell.tsx b/apps/backoffice-tokenization/src/components/layout/page-shell.tsx new file mode 100644 index 0000000..d3da4b6 --- /dev/null +++ b/apps/backoffice-tokenization/src/components/layout/page-shell.tsx @@ -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 ( + + + + +
{children}
+
+
+ ); +} diff --git a/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx b/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx new file mode 100644 index 0000000..453d818 --- /dev/null +++ b/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx @@ -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; + + const progress = mapCampaignProgress(campaign); + + const statusCfg = CAMPAIGN_STATUS_CONFIG[status]; + + return ( +
+ {/* Top row: status + action */} +
+ + {statusCfg.label} + + + +
+ + {/* Title & subtitle */} +
+

+ #{id.slice(0, 3).toUpperCase()} {title} +

+
+ + {/* Description */} +

+ {description} +

+ + {/* Bottom row: participants + escrow link | progress */} +
+ {/* Participants + escrow */} +
+ +
+ + {/* Progress */} +
+
+ + Loans Completed + + {progress}% +
+ +
+
+
+ ); +} diff --git a/apps/backoffice-tokenization/src/components/shared/section-title.tsx b/apps/backoffice-tokenization/src/components/shared/section-title.tsx new file mode 100644 index 0000000..2889b55 --- /dev/null +++ b/apps/backoffice-tokenization/src/components/shared/section-title.tsx @@ -0,0 +1,15 @@ +interface SectionTitleProps { + title: string; + description?: string; +} + +export function SectionTitle({ title, description }: SectionTitleProps) { + return ( +
+

{title}

+ {description && ( +

{description}

+ )} +
+ ); +} diff --git a/apps/backoffice-tokenization/src/components/shared/stat-item.tsx b/apps/backoffice-tokenization/src/components/shared/stat-item.tsx new file mode 100644 index 0000000..6dd8bee --- /dev/null +++ b/apps/backoffice-tokenization/src/components/shared/stat-item.tsx @@ -0,0 +1,17 @@ +interface StatItemProps { + label: string; + value: string | number; + description?: string; +} + +export function StatItem({ label, value, description }: StatItemProps) { + return ( +
+ {label} + {value} + {description && ( + {description} + )} +
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx new file mode 100644 index 0000000..0f3b8cc --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx @@ -0,0 +1,37 @@ +"use client"; + +import type { CampaignStatus } from "@/features/campaigns/types/campaign.types"; + +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" }, +]; + +interface CampaignFilterProps { + value: CampaignStatus | "all"; + onChange: (value: CampaignStatus | "all") => void; +} + +export function CampaignFilter({ value, onChange }: CampaignFilterProps) { + return ( +
+ {STATUS_OPTIONS.map((option) => ( + + ))} +
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx new file mode 100644 index 0000000..ce38abd --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx @@ -0,0 +1,30 @@ +import type { Campaign } from "@/features/campaigns/types/campaign.types"; +import { CampaignCard } from "@/components/shared/campaign-card"; + +interface CampaignListProps { + campaigns: Campaign[]; +} + +export function CampaignList({ campaigns }: CampaignListProps) { + if (campaigns.length === 0) { + return ( +
+

No hay campañas disponibles.

+
+ ); + } + + return ( +
+ {campaigns.map((campaign) => ( + + ))} +
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx new file mode 100644 index 0000000..b52ceb2 --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Search } from "lucide-react"; + +interface CampaignSearchProps { + value: string; + onChange: (value: string) => void; +} + +export function CampaignSearch({ value, onChange }: CampaignSearchProps) { + return ( +
+ + onChange(e.target.value)} + className="h-10 w-full rounded-xl border border-border bg-card pl-9 pr-4 text-sm text-foreground placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/campaign-toolbar.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-toolbar.tsx new file mode 100644 index 0000000..78fd114 --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/campaign-toolbar.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useState } from "react"; +import { CampaignSearch } from "./campaign-search"; +import { CampaignFilter } from "./campaign-filter"; +import type { CampaignStatus } from "@/features/campaigns/types/campaign.types"; + +interface CampaignToolbarProps { + onSearchChange: (value: string) => void; + onFilterChange: (value: CampaignStatus | "all") => void; +} + +export function CampaignToolbar({ onSearchChange, onFilterChange }: CampaignToolbarProps) { + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + + const handleSearchChange = (value: string) => { + setSearch(value); + onSearchChange(value); + }; + + const handleFilterChange = (value: CampaignStatus | "all") => { + setFilter(value); + onFilterChange(value); + }; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx new file mode 100644 index 0000000..ebc7c30 --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { StatItem } from "@/components/shared/stat-item"; +import { CampaignToolbar } from "./campaign-toolbar"; +import { CampaignList } from "./campaign-list"; +import { MOCK_CAMPAIGNS } from "@/features/campaigns/mock/campaigns.mock"; +import type { Campaign, CampaignStatus } from "@/features/campaigns/types/campaign.types"; + +export function CampaignsView() { + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + + const filtered = useMemo(() => { + return MOCK_CAMPAIGNS.filter((c) => { + const matchesStatus = filter === "all" || c.status === filter; + const matchesSearch = + search.trim() === "" || + c.title.toLowerCase().includes(search.toLowerCase()) || + c.description.toLowerCase().includes(search.toLowerCase()); + return matchesStatus && matchesSearch; + }); + }, [search, filter]); + + return ( +
+ {/* Toolbar */} + + + {/* List */} + +
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx new file mode 100644 index 0000000..f7f05b6 --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +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 ( + + ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx new file mode 100644 index 0000000..a77637e --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Loader2, PenLine } from "lucide-react"; +import { cn } from "@tokenization/shared/lib/utils"; +import { Button } from "@tokenization/ui/button"; +import { useCreateCampaign } from "@/features/campaigns/hooks/use-create-campaign"; +import { StepCampaignBasics } from "./step-campaign-basics"; +import { StepEscrowConfig } from "./step-escrow-config"; +import { StepCreateToken } from "./step-create-token"; + +const STEPS = [ + { number: 1, label: "Campaña Básica" }, + { number: 2, label: "Configuración Escrow" }, + { number: 3, label: "Crear Token" }, +]; + +export function CreateCampaignStepper() { + const router = useRouter(); + const { form, step, totalSteps, nextStep, prevStep, isSubmitting, error, onSubmit, totalCommitment } = + useCreateCampaign(); + + return ( +
+ {/* Step indicator */} +
+ {STEPS.map(({ number, label }, index) => ( +
+
number + ? "bg-accent text-foreground" + : "text-text-muted" + )} + > + number + ? "bg-foreground/10" + : "bg-border" + )} + > + {number} + + {label} +
+ {index < STEPS.length - 1 && ( +
number ? "bg-primary" : "bg-border" + )} + /> + )} +
+ ))} +
+ + {/* Step content */} +
+ {step === 1 && } + {step === 2 && ( + + )} + {step === 3 && } +
+ + {/* Error */} + {error && ( +

+ {error} +

+ )} + + {/* Navigation */} +
+ + + {step < totalSteps ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx new file mode 100644 index 0000000..9b59654 --- /dev/null +++ b/apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx @@ -0,0 +1,123 @@ +"use client"; + +import type { UseFormReturn } from "react-hook-form"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@tokenization/ui/form"; +import { Input } from "@tokenization/ui/input"; +import { Textarea } from "@tokenization/ui/textarea"; +import type { CreateCampaignFormValues } from "@/features/campaigns/types/campaign.types"; +import { numericInputKeyDown, parseNumericInput } from "@/lib/numeric-input"; + +interface Props { + form: UseFormReturn; +} + +export function StepCampaignBasics({ form }: Props) { + return ( +
+
+ ( + + Nombre de la Campaña + + + + + + )} + /> + + ( + + Descripción + +