diff --git a/.env.example b/.env.example index b2ee48f6..fa30dbd7 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,15 @@ - -# Firbease configuration -NEXT_PUBLIC_FIREBASE_API_KEY=TU_API_KEY -NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=TU_AUTH_DOMAIN -NEXT_PUBLIC_FIREBASE_PROJECT_ID=TU_PROJECT_ID -NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=TU_STORAGE_BUCKET -NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=TU_MESSAGING_SENDER_ID -NEXT_PUBLIC_FIREBASE_APP_ID=TU_APP_ID - -# Trustless Work API -NEXT_PUBLIC_API_URL=https://dev.api.trustlesswork.com -NEXT_PUBLIC_API_KEY=TU_API_KEY - - +# FIREBASE +NEXT_PUBLIC_FIREBASE_API_KEY= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= +NEXT_PUBLIC_FIREBASE_PROJECT_ID= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= +NEXT_PUBLIC_FIREBASE_APP_ID= + +# TRUSTLESS WORK +NEXT_PUBLIC_API_KEY= + +# MAINTENANCE MODE +NEXT_PUBLIC_MAINTENANCE_MODE=false +NEXT_PUBLIC_COUNTDOWN_HOURS=2 +NEXT_PUBLIC_COUNTDOWN_MINUTES=30 \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 37be5172..d35453c8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -27,6 +27,28 @@ Add here some information - **Video**: [Link to Loom video](https://loom.com) +--- + +## ✅ ESLint Compliance (Mandatory) + +To ensure that the code follows project standards, please run the following command and attach a screenshot of the output: + +```bash +npm run lint +``` + +You should see: + +``` +✔ No ESLint warnings or errors +``` + +📸 **Attach a screenshot showing the result of the lint check:** + +> ⚠️ **Pull requests without this screenshot will be rejected.** + +--- + ## 📂 Related Issue diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 5c44388a..00000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -engine-strict=true -npm=10.9.2 -save-exact=true diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 3872cc8b..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22.14.0 diff --git a/README.md b/README.md index 4bd17ff5..a2c5db91 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# 🚀 TrustBridge +![ ](https://github.com/user-attachments/assets/9201806d-7116-44d7-9df0-6f73c6f3d3f3) + +# TrustBridge **TrustBridge** is a decentralized lending platform built on the Stellar blockchain and integrated with Trustless Work for smart contract management. It enables users to request and fund secure loans, ensuring transparency, automation, and security without traditional intermediaries. @@ -76,7 +78,6 @@ NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=TU_MESSAGING_SENDER_ID NEXT_PUBLIC_FIREBASE_APP_ID=TU_APP_ID # Trustless Work API -NEXT_PUBLIC_API_URL=https://dev.api.trustlesswork.com NEXT_PUBLIC_API_KEY=TU_API_KEY ``` @@ -94,18 +95,6 @@ https://github.com/user-attachments/assets/0c4a8a80-33f1-41ae-819b-6a38abf30e4b --- -## 🔥 Firebase Setup - -Once you have your Firebase database ready, add the following document in the `trustlines` collection: - -``` -name: "USDC" (string) -trustline: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" (string) -trustlineDecimals: 10000000 (number) -``` - ---- - ## 🔑 Wallet Requirements To use this platform, install one of the following wallets: diff --git a/next.config.ts b/next.config.ts index e9ffa308..376d9317 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,22 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ +/** @type {import('next').NextConfig} */ +const nextConfig = { + env: { + NEXT_PUBLIC_MAINTENANCE_MODE: process.env.MAINTENANCE_MODE, + }, + // Add configuration for handling static files + async headers() { + return [ + { + source: "/locales/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=3600", // Cache for 1 hour + }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 659385a8..1dda209f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,7 @@ "eslint": "^9.23.0", "eslint-config-next": "15.2.4", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-prettier": "^5.2.5", + "eslint-plugin-prettier": "^5.4.1", "eslint-plugin-react": "^7.37.4", "globals": "^16.0.0", "husky": "^9.1.7", @@ -3120,9 +3120,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", "dev": true, "license": "MIT", "engines": { @@ -10766,14 +10766,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", - "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz", + "integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -15886,14 +15886,13 @@ } }, "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/package.json b/package.json index d4da0702..5c7b5195 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "eslint": "^9.23.0", "eslint-config-next": "15.2.4", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-prettier": "^5.2.5", + "eslint-plugin-prettier": "^5.4.1", "eslint-plugin-react": "^7.37.4", "globals": "^16.0.0", "husky": "^9.1.7", diff --git a/public/img/TrustBridge-dark.png b/public/img/TrustBridge-dark.png deleted file mode 100644 index 5ad98636..00000000 Binary files a/public/img/TrustBridge-dark.png and /dev/null differ diff --git a/public/img/TrustBridge-light.png b/public/img/TrustBridge-light.png deleted file mode 100644 index 2e5d29c4..00000000 Binary files a/public/img/TrustBridge-light.png and /dev/null differ diff --git a/public/img/TrustBridge.png b/public/img/TrustBridge.png index 69403c9c..19e01ab2 100644 Binary files a/public/img/TrustBridge.png and b/public/img/TrustBridge.png differ diff --git a/src/@types/chat.entity.ts b/src/@types/chat.entity.ts index f408696c..30af9d98 100644 --- a/src/@types/chat.entity.ts +++ b/src/@types/chat.entity.ts @@ -23,3 +23,21 @@ export interface ChatState { chats: Chat[]; activeChat: Chat | null; } + +export type ChatMessage = { + id: string; + from: string; + to: string; + content: string; + timestamp: number; + status?: "sent" | "delivered" | "read"; +}; + +export type ChatWithMessages = { + id: string; + participants: string[]; + lastMessage: string | null; + lastMessageTime: number | null; + createdAt: number; + messages: ChatMessage[]; +}; diff --git a/src/@types/user.entity.ts b/src/@types/user.entity.ts new file mode 100644 index 00000000..b03ba6e1 --- /dev/null +++ b/src/@types/user.entity.ts @@ -0,0 +1,22 @@ +export interface UserProfile { + walletAddress: string; + firstName: string; + lastName: string; + country: string; + phoneNumber: string; + createdAt: number; + updatedAt: number; +} + +export interface UserProfileFormData { + firstName: string; + lastName: string; + country: string; + phoneNumber: string; +} + +export interface UserChatData { + firstName: string; + lastName: string; + walletAddress: string; +} diff --git a/src/app/about-us/page.tsx b/src/app/about-us/page.tsx new file mode 100644 index 00000000..acd76f96 --- /dev/null +++ b/src/app/about-us/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import AboutPage from "@/components/modules/about-us/ui/pages/AboutUs"; +import { GradientBackground } from "@/components/modules/dashboard/ui/pages/background/GradientBackground"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +export default function Page() { + return ( + + +
+ +
+
+
+ ); +} diff --git a/src/app/dashboard/chat/[wallet]/page.tsx b/src/app/dashboard/chat/[wallet]/page.tsx new file mode 100644 index 00000000..ec3ca592 --- /dev/null +++ b/src/app/dashboard/chat/[wallet]/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { ChatDialog } from "@/components/modules/chat/ui/dialogs/ChatDialog"; + +export default function ChatPage() { + return ; +} diff --git a/src/app/dashboard/chat/page.tsx b/src/app/dashboard/chat/page.tsx index e3602911..ec3ca592 100644 --- a/src/app/dashboard/chat/page.tsx +++ b/src/app/dashboard/chat/page.tsx @@ -1,11 +1,6 @@ "use client"; - import { ChatDialog } from "@/components/modules/chat/ui/dialogs/ChatDialog"; export default function ChatPage() { - return ( -
- -
- ); + return ; } diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 9fa90159..9b50bf97 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,20 +1,16 @@ -"use client"; - -import type React from "react"; import { SidebarProvider } from "@/components/ui/sidebar"; import { TrustBridgeSidebar } from "@/components/layouts/sidebar/Sidebar"; import { Header } from "@/components/layouts/header/Header"; +import { ScrollArea } from "@/components/ui/scroll-area"; const Layout = ({ children }: { children: React.ReactNode }) => { return (
-
-
-
-
- {children} +
+
+ {children}
diff --git a/src/app/dashboard/marketplace/page.tsx b/src/app/dashboard/marketplace/page.tsx new file mode 100644 index 00000000..b24f210c --- /dev/null +++ b/src/app/dashboard/marketplace/page.tsx @@ -0,0 +1,5 @@ +import { MarketplacePage } from "@/components/modules/marketplace/ui/pages/MarketplacePage"; + +export default function Page() { + return ; +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index b4860960..c1a78e07 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,9 +1,5 @@ import { DashboardOverview } from "@/components/modules/dashboard/ui/pages/DashboardPage"; export default function Page() { - return ( -
- -
- ); + return ; } diff --git a/src/app/dashboard/profile/page.tsx b/src/app/dashboard/profile/page.tsx new file mode 100644 index 00000000..af499e34 --- /dev/null +++ b/src/app/dashboard/profile/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { UserProfileForm } from "@/components/modules/profile/ui/UserProfileForm"; +import { useWalletContext } from "@/providers/wallet.provider"; +import { Card } from "@/components/ui/card"; + +export default function SettingsPage() { + const { walletAddress } = useWalletContext(); + + if (!walletAddress) { + return ( +
+ +

+ Wallet not connected or invalid target. +

+
+
+ ); + } + + return ; +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fea..19e01ab2 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b09300ed..d3dc6414 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { GlobalProvider } from "@/providers/global.provider"; +import { Toaster } from "sonner"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -29,7 +30,10 @@ export default function RootLayout({ - {children} + + {children} + + ); diff --git a/src/app/maintenance/page.tsx b/src/app/maintenance/page.tsx new file mode 100644 index 00000000..064cb94f --- /dev/null +++ b/src/app/maintenance/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Bounded } from "@/components/layouts/Bounded"; +import CountdownTimer from "@/components/modules/maintenance/ui/CountdownTimer"; + +const Maintenance: React.FC = () => { + return ( + +
+

+ Site Under Maintenance +

+

+ We are making improvements to our platform to provide you with better + service. +

+ +
+
+ ); +}; + +export default Maintenance; diff --git a/src/app/page.tsx b/src/app/page.tsx index a634b998..202a0164 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { GradientBackground } from "@/components/modules/dashboard/ui/pages/back import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { useWalletContext } from "@/providers/wallet.provider"; +import HeaderHome from "@/components/layouts/header/HeaderHome"; export default function Page() { const { walletAddress } = useWalletContext(); @@ -18,9 +19,8 @@ export default function Page() { return ( -
- -
+ +
); } diff --git a/src/components/layouts/Bounded.tsx b/src/components/layouts/Bounded.tsx new file mode 100644 index 00000000..1c37c6af --- /dev/null +++ b/src/components/layouts/Bounded.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react"; + +type BoundedProps = { + children: ReactNode; + center?: boolean; + className?: string; +}; + +export const Bounded = ({ children, center, className }: BoundedProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/layouts/header/HeaderHome.tsx b/src/components/layouts/header/HeaderHome.tsx index 71d092e5..47f92f89 100644 --- a/src/components/layouts/header/HeaderHome.tsx +++ b/src/components/layouts/header/HeaderHome.tsx @@ -1,11 +1,20 @@ "use client"; -import ThemeToggle from "./ThemeToggle"; +import React from "react"; +import Link from "next/link"; -export function HeaderHome() { +const HeaderHome: React.FC = () => { return ( -
- +
+
+ + + +
); -} +}; + +export default HeaderHome; diff --git a/src/components/layouts/header/HeaderMaintenace.tsx b/src/components/layouts/header/HeaderMaintenace.tsx new file mode 100644 index 00000000..1fde6fbc --- /dev/null +++ b/src/components/layouts/header/HeaderMaintenace.tsx @@ -0,0 +1,14 @@ +"use client"; + +import Image from "next/image"; + +const HeaderMaintenace = () => { + return ( +
+ Trustless Work +
+
+ ); +}; + +export default HeaderMaintenace; diff --git a/src/components/layouts/header/ThemeToggle.tsx b/src/components/layouts/header/ThemeToggle.tsx deleted file mode 100644 index 82bab76c..00000000 --- a/src/components/layouts/header/ThemeToggle.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { MoonStar, Sun } from "lucide-react"; -import { useGlobalUIBoundedStore } from "@/core/store/ui"; - -const ThemeToggle = () => { - const theme = useGlobalUIBoundedStore((state) => state.theme); - const toggleTheme = useGlobalUIBoundedStore((state) => state.toggleTheme); - - useEffect(() => { - if (typeof window === "undefined") return; - - const root = window.document.documentElement; - root.classList.toggle("dark", theme === "dark"); - }, [theme]); - - return ( - - ); -}; - -export default ThemeToggle; diff --git a/src/components/layouts/sidebar/Sidebar.tsx b/src/components/layouts/sidebar/Sidebar.tsx index 9b539000..8d921595 100644 --- a/src/components/layouts/sidebar/Sidebar.tsx +++ b/src/components/layouts/sidebar/Sidebar.tsx @@ -13,6 +13,7 @@ import { SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; import { @@ -22,9 +23,10 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useTrustBridgeSidebar } from "./hooks/useSidebar.hook"; - +import { useUserContext } from "@/providers/user.provider"; export function TrustBridgeSidebar() { const { formattedAddress, collapsed, menuItems } = useTrustBridgeSidebar(); + const { profile } = useUserContext(); return ( @@ -50,78 +52,80 @@ export function TrustBridgeSidebar() {
- - {menuItems.map((section, idx) => ( - - {!collapsed && ( - - {section.section} - - )} - - - {section.items.map((item, itemIdx) => ( - - - - - - + + {menuItems.map((section, idx) => ( + + {!collapsed && ( + + {section.section} + + )} + + + {section.items.map((item, itemIdx) => ( + + + + + - - {item.icon} - - {(!collapsed || item.highlight) && ( - {item.label} + {item.icon} - )} - - - - - {collapsed && !item.highlight && ( - - {item.label} - - )} - - - ))} - - - - ))} - + {(!collapsed || item.highlight) && ( + + {item.label} + + )} + + + + + {collapsed && !item.highlight && ( + + {item.label} + + )} + + + ))} + + + + ))} + +
@@ -135,7 +139,9 @@ export function TrustBridgeSidebar() { {!collapsed && (
- User + + {profile?.firstName} {profile?.lastName} + {formattedAddress} diff --git a/src/components/layouts/sidebar/hooks/useSidebar.hook.tsx b/src/components/layouts/sidebar/hooks/useSidebar.hook.tsx index ab19bb59..b9aaf899 100644 --- a/src/components/layouts/sidebar/hooks/useSidebar.hook.tsx +++ b/src/components/layouts/sidebar/hooks/useSidebar.hook.tsx @@ -2,7 +2,13 @@ import { useState } from "react"; import { usePathname } from "next/navigation"; -import { LayoutDashboard, CreditCard, MessageSquare } from "lucide-react"; +import { + LayoutDashboard, + CreditCard, + MessageSquare, + Settings, + ShoppingCart, +} from "lucide-react"; import { ReactNode } from "react"; import { useWalletContext } from "@/providers/wallet.provider"; @@ -54,14 +60,19 @@ export function useTrustBridgeSidebar() { label: "Loans", active: pathname.startsWith("/dashboard/loans"), }, + { + href: "/dashboard/marketplace", + icon: , + label: "Marketplace", + active: pathname.startsWith("/dashboard/marketplace"), + }, ], }, - { section: "Communication", items: [ { - href: "/dashboard/chat", + href: "/dashboard/chat/[wallet]", icon: , label: "Chat", active: pathname === "/dashboard/chat", @@ -69,6 +80,17 @@ export function useTrustBridgeSidebar() { }, ], }, + { + section: "Settings", + items: [ + { + href: "/dashboard/profile", + icon: , + label: "Profile Settings", + active: pathname === "/dashboard/profile", + }, + ], + }, ]; const toggleCollapsed = () => setCollapsed(!collapsed); diff --git a/src/components/middleware.ts b/src/components/middleware.ts new file mode 100644 index 00000000..d599e453 --- /dev/null +++ b/src/components/middleware.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; + +export function middleware(request: NextRequest) { + const maintenanceMode = process.env.NEXT_PUBLIC_MAINTENANCE_MODE === "true"; + + // Maintenance mode activated + if ( + maintenanceMode && + request.nextUrl.pathname !== "/maintenance" && + !request.nextUrl.pathname.startsWith("/_next") && + !request.nextUrl.pathname.startsWith("/static") + ) { + return NextResponse.redirect(new URL("/maintenance", request.url)); + } + + // Maintenance mode deactivated + if (!maintenanceMode && request.nextUrl.pathname === "/maintenance") { + return NextResponse.redirect(new URL("/", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/", "/:path*"], +}; diff --git a/src/components/modules/about-us/hooks/useAbout.hook.ts b/src/components/modules/about-us/hooks/useAbout.hook.ts new file mode 100644 index 00000000..fb10654f --- /dev/null +++ b/src/components/modules/about-us/hooks/useAbout.hook.ts @@ -0,0 +1,66 @@ +"use client"; + +import { useEffect, useState } from "react"; + +// Types for About Us data +export interface TeamMember { + name: string; + role: string; +} + +export interface AboutData { + mission: string; + story: string; + team: TeamMember[]; + technology: string; +} + +// Mock API fetch function +async function fetchAboutData(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + mission: + "To empower individuals and communities with direct financial access by removing traditional barriers, fees, and intermediaries through a trustless system.", + story: `TrustBridge was born in 2024 when a group of friends passionate about blockchain technologies came together with a common vision: to create a bridge of trust between capital and opportunity.\n\nBuilt on the Stellar blockchain, we chose this technology for its speed, low cost, and focus on financial inclusion, values that are at the core of our identity.`, + team: [ + { + name: "Josué Brenes", + role: "CEO & FullStack Developer", + }, + { + name: "Yuliana Gonzáles", + role: "BackEnd Developer", + }, + { + name: "Daniel Coto", + role: "FrontEnd Developer", + }, + ], + technology: + "We use the Stellar blockchain to provide fast, secure, and low-cost transactions. Our platform is designed to be accessible to both experienced cryptocurrency users and those new to this technology.", + }); + }, 800); + }); +} + +export function useAboutData() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchAboutData() + .then((res) => { + setData(res); + setLoading(false); + }) + .catch((error) => { + console.error("Failed to fetch about data:", error); + setError("Failed to load about data"); + setLoading(false); + }); + }, []); + + return { data, loading, error }; +} diff --git a/src/components/modules/about-us/ui/pages/AboutUs.tsx b/src/components/modules/about-us/ui/pages/AboutUs.tsx new file mode 100644 index 00000000..4c419f69 --- /dev/null +++ b/src/components/modules/about-us/ui/pages/AboutUs.tsx @@ -0,0 +1,81 @@ +"use client"; +import Link from "next/link"; +import { TeamMember, useAboutData } from "../../hooks/useAbout.hook"; + +export default function AboutPage() { + const { data, loading, error } = useAboutData(); + + return ( +
+
+ + ← Back to Home + +

About Us

+ + {loading && ( +
+ Loading... +
+ )} + + {error && ( +
{error}
+ )} + + {data && ( + <> +
+

+ Our Mission +

+

{data.mission}

+
+ +
+

+ Our Story +

+

+ {data.story} +

+
+ +
+

+ Our Team +

+
+ {data.team.map((member: TeamMember) => ( +
+
+ + {member.name} + + + {member.role} + +
+ ))} +
+
+ +
+

+ Blockchain Technology +

+

{data.technology}

+
+ + )} +
+
+ ); +} diff --git a/src/components/modules/auth/hooks/wallet.hook.ts b/src/components/modules/auth/hooks/wallet.hook.ts index b7304e39..1e7bb333 100644 --- a/src/components/modules/auth/hooks/wallet.hook.ts +++ b/src/components/modules/auth/hooks/wallet.hook.ts @@ -1,6 +1,10 @@ import { kit } from "@/config/wallet-kit"; import { useWalletContext } from "@/providers/wallet.provider"; import { ISupportedWallet } from "@creit.tech/stellar-wallets-kit"; +import { db } from "@/lib/firebase"; +import { doc, getDoc, setDoc } from "firebase/firestore"; +import { UserProfile } from "@/@types/user.entity"; +import { toast } from "sonner"; export const useWallet = () => { // Get wallet info from wallet context @@ -19,6 +23,30 @@ export const useWallet = () => { const { name } = option; setWalletInfo(address, name); + + // Check if user profile exists and create if it doesn't + try { + const userDoc = await getDoc(doc(db, "users", address)); + + if (!userDoc.exists()) { + const now = Date.now(); + const initialProfile: UserProfile = { + walletAddress: address, + firstName: "", + lastName: "", + country: "", + phoneNumber: "", + createdAt: now, + updatedAt: now, + }; + + await setDoc(doc(db, "users", address), initialProfile); + toast.success("Welcome! Please complete your profile."); + } + } catch (error) { + console.error("Error creating initial profile:", error); + toast.error("Failed to create initial profile"); + } }, }); }; diff --git a/src/components/modules/auth/ui/pages/Home.tsx b/src/components/modules/auth/ui/pages/Home.tsx index 5b3ae236..4d468bb3 100644 --- a/src/components/modules/auth/ui/pages/Home.tsx +++ b/src/components/modules/auth/ui/pages/Home.tsx @@ -3,11 +3,10 @@ import { useWallet } from "@/components/modules/auth/hooks/wallet.hook"; import { useWalletContext } from "@/providers/wallet.provider"; import { useEffect } from "react"; -import { ArrowRight, Wallet, BadgeCheck, LogIn, LogOut } from "lucide-react"; +import { ArrowRight, Wallet, LogIn, LogOut } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import Link from "next/link"; -import Image from "next/image"; export default function HomePage() { const { walletAddress } = useWalletContext(); @@ -26,100 +25,83 @@ export default function HomePage() { return (
-
-
-
-
-
- - Powered by Stellar Blockchain - -

- - TrustBridge - - Decentralized Microloans -

-

- Connecting lenders and borrowers through secure, transparent, - and efficient blockchain technology. Build trust, create - opportunity. -

+
+
+
+
+ + Powered by Stellar Blockchain + + +

+ + TrustBridge + + Decentralized Microloans +

+ +

+ Connecting lenders and borrowers through secure, transparent, + and efficient blockchain technology. Build trust, create + opportunity. +

+
+ + {/* Mobile wallet address display */} + {walletAddress && ( +
+ + + {truncateAddress(walletAddress)} +
+ )} -
- {walletAddress ? ( - <> -
- - - {truncateAddress(walletAddress)} - -
- - - ) : ( - - )} +
+ {walletAddress ? ( + <> + {/* Desktop wallet address display */} +
+ + + {truncateAddress(walletAddress)} + +
- - -
-
- -
-
-
-
-
- -
-
-

- Total Loan Volume -

-

$1.2M+

-
-
-
+ + ) : ( + + )} - {/* Imagen decorativa o espacio para gráficos */} -
- Loan Illustration -
-
+ + +
diff --git a/src/components/modules/chat/hooks/chat.hook.ts b/src/components/modules/chat/hooks/chat.hook.ts new file mode 100644 index 00000000..84552ed0 --- /dev/null +++ b/src/components/modules/chat/hooks/chat.hook.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; +import { fetchMessages, sendMessage } from "../lib/chat"; + +type ChatMessage = { + id: string; + from: string; + to: string; + content: string; + timestamp: number; + status?: string; +}; + +export const useChat = (walletA: string, walletB: string) => { + const [messages, setMessages] = useState([]); + + const loadMessages = async () => { + const msgs = await fetchMessages(walletA, walletB); + setMessages(msgs); + }; + + const handleSend = async (message: string) => { + await sendMessage(walletA, walletB, message); + await loadMessages(); + }; + + useEffect(() => { + loadMessages(); + }, [walletA, walletB]); + + return { messages, handleSend }; +}; diff --git a/src/components/modules/chat/hooks/use-all-chats.hook.ts b/src/components/modules/chat/hooks/use-all-chats.hook.ts new file mode 100644 index 00000000..f29b0ffc --- /dev/null +++ b/src/components/modules/chat/hooks/use-all-chats.hook.ts @@ -0,0 +1,74 @@ +import { useEffect, useState } from "react"; +import { onSnapshot, query, collection, where } from "firebase/firestore"; +import { db } from "@/lib/firebase"; +import { ChatMessage, ChatWithMessages } from "@/@types/chat.entity"; + +export const useAllChats = (walletAddress: string) => { + const [chats, setChats] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!walletAddress) { + setLoading(false); + return; + } + + let unsubscribe: (() => void) | undefined; + + const setupChats = async () => { + try { + setLoading(true); + setError(null); + + // Set up real-time listener for all chats + const chatsRef = collection(db, "chats"); + const q = query( + chatsRef, + where("participants", "array-contains", walletAddress), + ); + + unsubscribe = onSnapshot( + q, + async (snapshot) => { + const updatedChats = await Promise.all( + snapshot.docs.map(async (doc) => { + const chatData = doc.data() as Omit< + ChatWithMessages, + "id" | "messages" + >; + const messages: ChatMessage[] = []; + + return { + id: doc.id, + ...chatData, + messages, + } as ChatWithMessages; + }), + ); + + setChats(updatedChats); + setLoading(false); + }, + (err) => { + console.error("Error listening to chats:", err); + setError("Error listening to chats"); + setLoading(false); + }, + ); + } catch (err) { + console.error("Error setting up chats:", err); + setError("Error setting up chats"); + setLoading(false); + } + }; + + setupChats(); + + return () => { + if (unsubscribe) unsubscribe(); + }; + }, [walletAddress]); + + return { chats, loading, error }; +}; diff --git a/src/components/modules/chat/hooks/wallet-chat.hook.ts b/src/components/modules/chat/hooks/wallet-chat.hook.ts new file mode 100644 index 00000000..5dbbd167 --- /dev/null +++ b/src/components/modules/chat/hooks/wallet-chat.hook.ts @@ -0,0 +1,74 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { listenToMessages, sendMessage } from "../lib/chat"; + +type ChatMessage = { + id: string; + from: string; + to: string; + content: string; + timestamp: number; + status?: "sent" | "delivered" | "read"; +}; + +export const useWalletChat = (walletA: string, walletB: string) => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if ( + !walletA || + !walletB || + walletA === "[wallet]" || + walletB === "[wallet]" + ) { + setLoading(false); + return; + } + + let unsubscribe: (() => void) | undefined; + + const setupChat = async () => { + try { + setLoading(true); + setError(null); + + unsubscribe = await listenToMessages( + walletA, + walletB, + (newMessages: ChatMessage[]) => { + setMessages(newMessages); + setLoading(false); + }, + ); + } catch (err) { + console.error("Error setting up chat:", err); + setError("Error setting up chat"); + setLoading(false); + } + }; + + setupChat(); + + return () => { + if (unsubscribe) unsubscribe(); + }; + }, [walletA, walletB]); + + const send = async (content: string) => { + try { + setError(null); + await sendMessage(walletA, walletB, content); + // No necesitamos actualizar el estado local aquí porque listenToMessages + // se encargará de actualizar los mensajes en tiempo real + } catch (err) { + console.error("Error sending message:", err); + setError("Error sending message"); + throw err; + } + }; + + return { messages, sendMessage: send, loading, error }; +}; diff --git a/src/components/modules/chat/lib/chat.ts b/src/components/modules/chat/lib/chat.ts new file mode 100644 index 00000000..dafe4762 --- /dev/null +++ b/src/components/modules/chat/lib/chat.ts @@ -0,0 +1,280 @@ +import { UserChatData } from "@/@types/user.entity"; +import { db } from "@/lib/firebase"; +import { + collection, + addDoc, + query, + orderBy, + onSnapshot, + getDocs, + doc, + setDoc, + getDoc, + Unsubscribe, + where, + serverTimestamp, + Timestamp, +} from "firebase/firestore"; + +export type ChatMessage = { + id: string; + from: string; + to: string; + content: string; + timestamp: number; + status?: "sent" | "delivered" | "read"; +}; + +export const getChatId = (walletA: string, walletB: string) => { + if ( + !walletA || + !walletB || + walletA === "[wallet]" || + walletB === "[wallet]" + ) { + throw new Error("Invalid wallet addresses"); + } + const sortedWallets = [walletA, walletB].sort(); + return sortedWallets.join("_"); +}; + +export const initializeChat = async (walletA: string, walletB: string) => { + if ( + !walletA || + !walletB || + walletA === "[wallet]" || + walletB === "[wallet]" + ) { + throw new Error("Invalid wallet addresses"); + } + + const chatId = getChatId(walletA, walletB); + const chatDocRef = doc(db, "chats", chatId); + + try { + const chatDoc = await getDoc(chatDocRef); + + if (!chatDoc.exists()) { + await setDoc(chatDocRef, { + participants: [walletA, walletB].sort(), + createdAt: serverTimestamp(), + lastMessage: null, + lastMessageTime: null, + }); + } + + return chatId; + } catch (error) { + console.error("Error initializing chat:", error); + throw error; + } +}; + +export const sendMessage = async ( + from: string, + to: string, + content: string, +) => { + if (!from || !to || from === "[wallet]" || to === "[wallet]") { + throw new Error("Invalid wallet addresses"); + } + + try { + const chatId = await initializeChat(from, to); + const messagesCol = collection(db, "chats", chatId, "messages"); + + const messageData = { + from, + to, + content, + timestamp: serverTimestamp(), + status: "sent", + }; + + await addDoc(messagesCol, messageData); + + const chatDocRef = doc(db, "chats", chatId); + await setDoc( + chatDocRef, + { + lastMessage: content, + lastMessageTime: serverTimestamp(), + }, + { merge: true }, + ); + + return messageData; + } catch (error) { + console.error("Error sending message:", error); + throw error; + } +}; + +const convertTimestamp = (timestamp: Timestamp | Date | number): number => { + if (timestamp instanceof Timestamp) { + return timestamp.toMillis(); + } + if (timestamp instanceof Date) { + return timestamp.getTime(); + } + if (typeof timestamp === "number") { + return timestamp; + } + return Date.now(); +}; + +export const fetchMessages = async ( + walletA: string, + walletB: string, +): Promise => { + if ( + !walletA || + !walletB || + walletA === "[wallet]" || + walletB === "[wallet]" + ) { + return []; + } + + const chatId = getChatId(walletA, walletB); + const chatDocRef = doc(db, "chats", chatId); + const chatDoc = await getDoc(chatDocRef); + + if (!chatDoc.exists()) { + return []; + } + + const messagesCol = collection(db, "chats", chatId, "messages"); + const q = query(messagesCol, orderBy("timestamp", "asc")); + const snapshot = await getDocs(q); + + return snapshot.docs.map((doc) => { + const data = doc.data(); + return { + id: doc.id, + from: data.from, + to: data.to, + content: data.content, + timestamp: convertTimestamp(data.timestamp), + status: data.status || "sent", + }; + }); +}; + +export const listenToMessages = async ( + walletA: string, + walletB: string, + callback: (messages: ChatMessage[]) => void, +): Promise => { + if ( + !walletA || + !walletB || + walletA === "[wallet]" || + walletB === "[wallet]" + ) { + callback([]); + return () => {}; + } + + const chatId = getChatId(walletA, walletB); + const chatDocRef = doc(db, "chats", chatId); + const chatDoc = await getDoc(chatDocRef); + + if (!chatDoc.exists()) { + await setDoc(chatDocRef, { + participants: [walletA, walletB].sort(), + createdAt: serverTimestamp(), + lastMessage: null, + lastMessageTime: null, + }); + } + + const messagesCol = collection(db, "chats", chatId, "messages"); + const q = query(messagesCol, orderBy("timestamp", "asc")); + + const unsubscribe = onSnapshot( + q, + (snapshot) => { + const msgs: ChatMessage[] = snapshot.docs.map((doc) => { + const data = doc.data(); + return { + id: doc.id, + from: data.from, + to: data.to, + content: data.content, + timestamp: convertTimestamp(data.timestamp), + status: data.status || "sent", + }; + }); + callback(msgs); + }, + (error) => { + console.error("Error listening to messages:", error); + }, + ); + + return unsubscribe; +}; + +export const getUserChats = async (walletAddress: string) => { + if (!walletAddress || walletAddress === "[wallet]") { + return []; + } + + const chatsRef = collection(db, "chats"); + const q = query( + chatsRef, + where("participants", "array-contains", walletAddress), + ); + + const snapshot = await getDocs(q); + return snapshot.docs.map((doc) => { + const data = doc.data(); + return { + id: doc.id, + participants: data.participants, + createdAt: convertTimestamp(data.createdAt), + lastMessage: data.lastMessage, + lastMessageTime: convertTimestamp(data.lastMessageTime), + }; + }); +}; + +export const getUserData = async ( + walletAddress: string, +): Promise => { + if (!walletAddress || walletAddress === "[wallet]") { + return { + firstName: "Unknown", + lastName: "", + walletAddress: "", + }; + } + + try { + const userDocRef = doc(db, "users", walletAddress); + const userDoc = await getDoc(userDocRef); + + if (userDoc.exists()) { + const data = userDoc.data(); + return { + firstName: data.firstName || "Unknown", + lastName: data.lastName || "", + walletAddress: data.walletAddress || walletAddress, + }; + } + + return { + firstName: "Unknown", + lastName: "", + walletAddress, + }; + } catch (error) { + console.error("Error getting user data:", error); + return { + firstName: "Unknown", + lastName: "", + walletAddress, + }; + } +}; diff --git a/src/components/modules/chat/ui/components/chat-list.tsx b/src/components/modules/chat/ui/components/chat-list.tsx new file mode 100644 index 00000000..0053b1e0 --- /dev/null +++ b/src/components/modules/chat/ui/components/chat-list.tsx @@ -0,0 +1,130 @@ +import { useRouter, useParams } from "next/navigation"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Card } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ChatWithMessages } from "@/@types/chat.entity"; +import { formatAddress } from "@/lib/utils"; +import { cn } from "@/lib/utils"; +import { useEffect, useState } from "react"; +import { getUserData } from "../../lib/chat"; +import { UserChatData } from "@/@types/user.entity"; + +interface ChatListProps { + chats: ChatWithMessages[]; + currentWallet: string; + loading: boolean; +} + +export function ChatList({ chats, currentWallet, loading }: ChatListProps) { + const router = useRouter(); + const { wallet: activeWallet } = useParams(); + const [userData, setUserData] = useState>({}); + + useEffect(() => { + const fetchUserData = async () => { + const newUserData: Record = {}; + + for (const chat of chats) { + const otherParticipant = chat.participants.find( + (p) => p !== currentWallet, + ); + + if (otherParticipant && !userData[otherParticipant]) { + const data = await getUserData(otherParticipant); + newUserData[otherParticipant] = data; + } + } + + setUserData((prev) => ({ ...prev, ...newUserData })); + }; + + if (chats.length > 0) { + fetchUserData(); + } + }, [chats, currentWallet]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (chats.length === 0) { + return ( +
+

+ No chats yet. Start a new conversation by entering a wallet address + above. +

+
+ ); + } + + return ( + +
+ {chats.map((chat) => { + const otherParticipant = chat.participants.find( + (p) => p !== currentWallet, + ); + const lastMessageTime = chat.lastMessageTime + ? new Date(chat.lastMessageTime).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }) + : null; + const isActive = otherParticipant === activeWallet; + const user = userData[otherParticipant || ""]; + + return ( + router.push(`/dashboard/chat/${otherParticipant}`)} + > +
+ + + + {user?.firstName?.slice(0, 2).toUpperCase() || + otherParticipant?.slice(0, 2).toUpperCase()} + {user?.lastName?.slice(0, 2).toUpperCase() || + otherParticipant?.slice(0, 2).toUpperCase()} + + +
+
+
+

+ {user?.firstName} {user?.lastName} +

+

+ {formatAddress(otherParticipant || "")} +

+
+ {lastMessageTime && ( + {lastMessageTime} + )} +
+

+ {chat.lastMessage || "No messages yet"} +

+
+
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/modules/chat/ui/components/message-bubble.tsx b/src/components/modules/chat/ui/components/message-bubble.tsx index 16aa586b..7d61d761 100644 --- a/src/components/modules/chat/ui/components/message-bubble.tsx +++ b/src/components/modules/chat/ui/components/message-bubble.tsx @@ -1,58 +1,67 @@ "use client"; import { cn } from "@/lib/utils"; -import { Check, CheckCheck, Clock, AlertCircle } from "lucide-react"; -import { Message } from "@/@types/chat.entity"; +import { Check, CheckCheck, Clock } from "lucide-react"; + +interface Message { + id: string; + content: string; + sender: "user" | "other"; + timestamp: string; + status: "sent" | "delivered" | "read"; +} interface MessageBubbleProps { message: Message; - onRetry?: (messageId: string) => void; } -export function MessageBubble({ message, onRetry }: MessageBubbleProps) { +export function MessageBubble({ message }: MessageBubbleProps) { + const isUser = message.sender === "user"; + const getStatusIcon = () => { switch (message.status) { - case "sending": - return ; case "sent": - return ; + return ; case "delivered": - return ; + return ; case "read": - return ; - case "error": - return ; + return ; default: - return null; + return ; } }; return ( -
-
-

{message.content}

-
- {message.timestamp} - {message.sender === "user" && getStatusIcon()} - {message.status === "error" && onRetry && ( - + {getStatusIcon()} + )}
diff --git a/src/components/modules/chat/ui/dialogs/ChatDialog.tsx b/src/components/modules/chat/ui/dialogs/ChatDialog.tsx index a2b5d57b..5a29160c 100644 --- a/src/components/modules/chat/ui/dialogs/ChatDialog.tsx +++ b/src/components/modules/chat/ui/dialogs/ChatDialog.tsx @@ -1,396 +1,325 @@ "use client"; -import { cn } from "@/lib/utils"; +import { useState, useRef, useEffect } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Send, Menu } from "lucide-react"; -import { useState, useRef, useEffect } from "react"; -import { Chat, Message, ChatState } from "@/@types/chat.entity"; -import Loader from "@/components/utils/ui/Loader"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Send, Menu, ArrowLeft, Copy, Check } from "lucide-react"; import { MessageBubble } from "../components/message-bubble"; - -const mockChats: Chat[] = [ - { - id: "1", - name: "John Doe", - avatar: "https://github.com/shadcn.png", - lastMessage: "Hey, how are you?", - unread: 2, - status: "online", - messages: [ - { - id: "1", - content: "Hey, how are you?", - sender: "other", - timestamp: "10:30 AM", - status: "read", - }, - { - id: "2", - content: "I'm good, thanks! How about you?", - sender: "user", - timestamp: "10:32 AM", - status: "read", - }, - ], - }, - { - id: "2", - name: "Jane Smith", - avatar: "https://github.com/shadcn.png", - lastMessage: "Can we discuss the loan terms?", - unread: 0, - status: "offline", - lastSeen: "2 hours ago", - messages: [ - { - id: "1", - content: "Can we discuss the loan terms?", - sender: "other", - timestamp: "9:15 AM", - status: "delivered", - }, - ], - }, -]; +import { useWalletContext } from "@/providers/wallet.provider"; +import { useParams, useRouter } from "next/navigation"; +import { useWalletChat } from "../../hooks/wallet-chat.hook"; +import { useAllChats } from "../../hooks/use-all-chats.hook"; +import { ChatList } from "../components/chat-list"; +import { toast } from "sonner"; +import { getUserData } from "../../lib/chat"; +import { UserChatData } from "@/@types/user.entity"; export function ChatDialog() { - const [state, setState] = useState({ - isLoading: false, - error: null, - chats: mockChats, - activeChat: mockChats[0], - }); + const { walletAddress } = useWalletContext(); + const { wallet: otherWallet } = useParams(); const [message, setMessage] = useState(""); + const [targetWallet, setTargetWallet] = useState(""); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isSending, setIsSending] = useState(false); + const [copiedAddress, setCopiedAddress] = useState(false); + const [userData, setUserData] = useState(null); const messageEndRef = useRef(null); + const inputRef = useRef(null); + const router = useRouter(); - const scrollToBottom = () => { - messageEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; + const { chats, loading: chatsLoading } = useAllChats(walletAddress!); + const { + messages, + sendMessage, + loading: messagesLoading, + error, + } = useWalletChat(walletAddress!, otherWallet as string); useEffect(() => { - scrollToBottom(); - }, [state.activeChat?.messages]); - - const simulateSendMessage = (message: Message, chatToUpdate: Chat) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - try { - if (Math.random() < 0.2) { - throw new Error("Failed to send message"); - } - - const sentMessage = { ...message, status: "sent" as const }; - const updatedChatWithSent = { - ...chatToUpdate, - messages: chatToUpdate.messages.map((msg) => - msg.id === message.id ? sentMessage : msg, - ), - }; - - setState((prev) => ({ - ...prev, - chats: prev.chats.map((chat) => - chat.id === updatedChatWithSent.id ? updatedChatWithSent : chat, - ), - activeChat: - prev.activeChat?.id === updatedChatWithSent.id - ? updatedChatWithSent - : prev.activeChat, - })); - - setTimeout(() => { - const deliveredMessage = { - ...sentMessage, - status: "delivered" as const, - }; - const updatedChatWithDelivered = { - ...updatedChatWithSent, - messages: updatedChatWithSent.messages.map((msg) => - msg.id === sentMessage.id ? deliveredMessage : msg, - ), - }; + if (error) { + toast.error(error); + } + }, [error]); - setState((prev) => ({ - ...prev, - chats: prev.chats.map((chat) => - chat.id === updatedChatWithDelivered.id - ? updatedChatWithDelivered - : chat, - ), - activeChat: - prev.activeChat?.id === updatedChatWithDelivered.id - ? updatedChatWithDelivered - : prev.activeChat, - })); - resolve(); - }, 1000); - } catch (error: unknown) { - console.error("Error sending message:", error); - const errorMessage = { ...message, status: "error" as const }; - const updatedChatWithError = { - ...chatToUpdate, - messages: chatToUpdate.messages.map((msg) => - msg.id === message.id ? errorMessage : msg, - ), - }; - setState((prev) => ({ - ...prev, - chats: prev.chats.map((chat) => - chat.id === updatedChatWithError.id ? updatedChatWithError : chat, - ), - activeChat: - prev.activeChat?.id === updatedChatWithError.id - ? updatedChatWithError - : prev.activeChat, - })); - reject(error); - } - }, 1000); - }); - }; - - const handleSendMessage = () => { - if (!message.trim() || !state.activeChat) return; - - const newMessage: Message = { - id: Date.now().toString(), - content: message, - sender: "user", - timestamp: new Date().toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }), - status: "sending", - }; - - // Update active chat with new message - const updatedChat = { - ...state.activeChat, - lastMessage: newMessage.content, - unread: 0, - messages: [...state.activeChat.messages, newMessage], + useEffect(() => { + const fetchUserData = async () => { + if (otherWallet) { + const data = await getUserData(otherWallet as string); + setUserData(data); + } }; - // Update chats list - const updatedChats = state.chats.map((chat) => - chat.id === updatedChat.id ? updatedChat : chat, - ); + fetchUserData(); + }, [otherWallet]); - setState((prev) => ({ - ...prev, - chats: updatedChats, - activeChat: updatedChat, - })); + const handleSendMessage = async () => { + if (!message.trim() || isSending) return; - setMessage(""); + try { + setIsSending(true); + await sendMessage(message); + setMessage(""); + inputRef.current?.focus(); + } catch { + toast.error("Failed to send message"); + } finally { + setIsSending(false); + } + }; - // Usa la función común - simulateSendMessage(newMessage, updatedChat).catch(() => {}); + const handleStartChat = () => { + if (!targetWallet.trim()) return; + router.push(`/dashboard/chat/${targetWallet}`); + setTargetWallet(""); }; - const handleRetryMessage = (messageId: string) => { - if (!state.activeChat) return; - const failedMessage = state.activeChat.messages.find( - (msg) => msg.id === messageId && msg.status === "error", - ); - if (!failedMessage) return; - const retryMessage: Message = { - ...failedMessage, - id: Date.now().toString(), - status: "sending", - }; - const updatedChat = { - ...state.activeChat, - messages: [...state.activeChat.messages, retryMessage], - }; - const updatedChats = state.chats.map((chat) => - chat.id === updatedChat.id ? updatedChat : chat, - ); - setState((prev) => ({ - ...prev, - chats: updatedChats, - activeChat: updatedChat, - })); - simulateSendMessage(retryMessage, updatedChat).catch(() => {}); + const copyAddress = async (address: string) => { + try { + await navigator.clipboard.writeText(address); + setCopiedAddress(true); + toast.success("Address copied to clipboard"); + setTimeout(() => setCopiedAddress(false), 2000); + } catch { + toast.error("Failed to copy address"); + } }; - if (state.isLoading) { - return ; - } + const formatAddress = (address: string) => { + if (!address) return ""; + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; - if (state.error) { + if (!walletAddress) { return (
-

{state.error}

+ +

+ Wallet not connected. +

+
); } + const loading = chatsLoading || messagesLoading; + return ( -
+
{/* Mobile Menu Button */} - {/* Chat List */} -
-
-

- Chats -

-
- - {state.chats.map((chat) => ( -
{ - setState((prev) => ({ ...prev, activeChat: chat })); - setIsMobileMenuOpen(false); - }} - > -
- - - {chat.name[0]} - - {chat.status === "online" && ( -
- )} -
-
-
-

- {chat.name} -

- - {chat.messages[chat.messages.length - 1]?.timestamp} - -
-
-

- {chat.lastMessage} -

- {chat.status === "offline" && chat.lastSeen && ( - - · {chat.lastSeen} - - )} -
-
- {chat.unread > 0 && ( -
- {chat.unread} -
- )} -
- ))} - -
- {/* Chat Window */} -
- {state.activeChat ? ( - <> - {/* Chat Header */} -
-
- - - {state.activeChat.name[0]} - - {state.activeChat.status === "online" && ( -
- )} -
-
-

- {state.activeChat.name} -

-

- {state.activeChat.status === "online" - ? "Online" - : state.activeChat.lastSeen - ? `Last seen ${state.activeChat.lastSeen}` - : "Offline"} -

-
-
- - {/* Messages */} - -
- {state.activeChat.messages.map((message) => ( - - ))} -
-
- - - {/* Message Input */} -
-
+
+ {/* Chat List Sidebar */} +
+ + +
setMessage(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSendMessage()} - className="flex-1 bg-muted border text-foreground placeholder:text-muted-foreground focus:ring-emerald-600 focus:border-emerald-600" + value={targetWallet} + onChange={(e) => setTargetWallet(e.target.value)} + placeholder="Enter wallet address to start new chat" + className="flex-1" />
+
+
+ + +
+ + {/* Main Chat Content */} +
+ {!otherWallet ? ( +
+ +

+ Select a chat or start a new conversation. +

+
- - ) : ( -
-

Select a chat to start messaging

-
- )} -
+ ) : ( + + {/* Header */} + +
+ - {/* Overlay for mobile menu */} - {isMobileMenuOpen && ( -
setIsMobileMenuOpen(false)} - /> - )} +
+ + + + {userData?.firstName?.slice(0, 2).toUpperCase() || + (otherWallet as string).slice(0, 2).toUpperCase()} + {userData?.lastName?.slice(0, 2).toUpperCase()} + + +
+
+ +
+
+

+ {userData?.firstName || + formatAddress(otherWallet as string)}{" "} + {userData?.lastName} +

+ +
+
+ + Wallet Chat + + + {formatAddress(otherWallet as string)} + + + {messages.length} messages + +
+
+
+ + + {/* Messages */} + + +
+ {loading && messages.length === 0 ? ( +
+
+
+ ) : messages.length === 0 ? ( +
+
+ +
+

+ Start the conversation +

+

+ Send your first message to{" "} + {userData?.firstName || + formatAddress(otherWallet as string)}{" "} + {userData?.lastName} + to begin chatting. +

+
+ ) : ( + messages.map((msg) => ( + + )) + )} +
+
+ + + + {/* Input */} +
+
+
+ setMessage(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }} + disabled={isSending} + className="pr-12 bg-background border-border focus:ring-emerald-600 focus:border-emerald-600" + /> + {message.trim() && ( +
+ {message.length}/500 +
+ )} +
+ +
+
+ Press Enter to send, Shift + Enter for new line + {error && ( + Failed to send message + )} +
+
+ + )} +
+
); } diff --git a/src/components/modules/dashboard/hooks/useDashboard.hook.ts b/src/components/modules/dashboard/hooks/useDashboard.hook.ts index 33248494..75e2879e 100644 --- a/src/components/modules/dashboard/hooks/useDashboard.hook.ts +++ b/src/components/modules/dashboard/hooks/useDashboard.hook.ts @@ -2,215 +2,52 @@ import { useState, useEffect } from "react"; import { useWalletContext } from "@/providers/wallet.provider"; - -interface DashboardStats { - totalLoans: number; - activeLoans: number; - totalAmount: number; - availableBalance: number; - pendingApprovals: number; - completedLoans: number; -} - -interface RecentActivity { - id: string; - type: - | "loan_created" - | "loan_approved" - | "milestone_completed" - | "payment_received" - | "loan_completed"; - title: string; - description: string; - amount?: number; - date: Date; - status?: "pending" | "completed" | "rejected"; -} - -interface LoanData { - month: string; - amount: number; +import { useUserContext } from "@/providers/user.provider"; +import { getUserChats } from "@/components/modules/chat/lib/chat"; +import { UserProfile } from "@/@types/user.entity"; + +interface DashboardData { + profile: UserProfile | null; + chatCount: number; + address: string | null; + walletName: string | null; + loading: boolean; } -interface UpcomingMilestone { - id: string; - loanTitle: string; - description: string; - dueDate: Date; - amount: number; -} +export function useDashboard(): DashboardData { + const { walletAddress: address, walletName } = useWalletContext(); + const { profile, loading: profileLoading } = useUserContext(); -export function useDashboard() { - const { walletAddress: address } = useWalletContext(); - const [loading, setLoading] = useState(true); - const [stats, setStats] = useState({ - totalLoans: 0, - activeLoans: 0, - totalAmount: 0, - availableBalance: 0, - pendingApprovals: 0, - completedLoans: 0, - }); - const [recentActivity, setRecentActivity] = useState([]); - const [loanTrends, setLoanTrends] = useState([]); - const [upcomingMilestones, setUpcomingMilestones] = useState< - UpcomingMilestone[] - >([]); + const [chatCount, setChatCount] = useState(0); + const [chatsLoading, setChatsLoading] = useState(true); useEffect(() => { - const loadDashboardData = async () => { - setLoading(true); - - setTimeout(() => { - setStats({ - totalLoans: 24, - activeLoans: 8, - totalAmount: 125000, - availableBalance: 42500, - pendingApprovals: 3, - completedLoans: 16, - }); - - setRecentActivity([ - { - id: "1", - type: "loan_approved", - title: "Loan Approved", - description: - 'Your loan offer for "Business Expansion Funding" was approved', - amount: 15000, - date: new Date(Date.now() - 1000 * 60 * 60 * 2), - status: "completed", - }, - { - id: "2", - type: "milestone_completed", - title: "Milestone Completed", - description: 'Milestone 2: "Product Development" was completed', - amount: 5000, - date: new Date(Date.now() - 1000 * 60 * 60 * 24), - status: "completed", - }, - { - id: "3", - type: "payment_received", - title: "Payment Received", - description: 'You received a payment for "Inventory Financing"', - amount: 2500, - date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), - status: "completed", - }, - { - id: "4", - type: "loan_created", - title: "Loan Created", - description: - 'You created a new loan offer for "Seasonal Inventory"', - amount: 7500, - date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), - status: "pending", - }, - { - id: "5", - type: "loan_completed", - title: "Loan Completed", - description: 'Loan "Working Capital" was fully repaid', - amount: 10000, - date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5), - status: "completed", - }, - ]); - - setLoanTrends([ - { month: "Jan", amount: 15000 }, - { month: "Feb", amount: 18000 }, - { month: "Mar", amount: 22000 }, - { month: "Apr", amount: 17000 }, - { month: "May", amount: 25000 }, - { month: "Jun", amount: 28000 }, - ]); - - setUpcomingMilestones([ - { - id: "1", - loanTitle: "Business Expansion Funding", - description: "Market Research Completion", - dueDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 2), - amount: 3000, - }, - { - id: "2", - loanTitle: "Tech Startup Investment", - description: "MVP Development", - dueDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 5), - amount: 5000, - }, - { - id: "3", - loanTitle: "Inventory Financing", - description: "Supplier Payment Confirmation", - dueDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), - amount: 2500, - }, - ]); - - setLoading(false); - }, 1500); + const loadChats = async () => { + if (!address) { + setChatCount(0); + setChatsLoading(false); + return; + } + + try { + const chats = await getUserChats(address); + setChatCount(chats.length); + } catch (err) { + console.error("Error loading chats:", err); + setChatCount(0); + } finally { + setChatsLoading(false); + } }; - loadDashboardData(); - }, []); - - const formatCurrency = (amount: number): string => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - maximumFractionDigits: 0, - }).format(amount); - }; - - const formatDate = (date: Date): string => { - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffSecs = Math.floor(diffMs / 1000); - const diffMins = Math.floor(diffSecs / 60); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffDays > 0) { - return diffDays === 1 ? "Yesterday" : `${diffDays} days ago`; - } else if (diffHours > 0) { - return `${diffHours} ${diffHours === 1 ? "hour" : "hours"} ago`; - } else if (diffMins > 0) { - return `${diffMins} ${diffMins === 1 ? "minute" : "minutes"} ago`; - } else { - return "Just now"; - } - }; - - const formatDueDate = (date: Date): string => { - const now = new Date(); - const diffMs = date.getTime() - now.getTime(); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) { - return "Today"; - } else if (diffDays === 1) { - return "Tomorrow"; - } else { - return `In ${diffDays} days`; - } - }; + loadChats(); + }, [address]); return { - loading, - stats, - recentActivity, - loanTrends, - upcomingMilestones, - formatCurrency, - formatDate, - formatDueDate, + profile, + chatCount, address, + walletName, + loading: profileLoading || chatsLoading, }; } diff --git a/src/components/modules/dashboard/ui/pages/DashboardPage.tsx b/src/components/modules/dashboard/ui/pages/DashboardPage.tsx index 8da9b42d..aab34429 100644 --- a/src/components/modules/dashboard/ui/pages/DashboardPage.tsx +++ b/src/components/modules/dashboard/ui/pages/DashboardPage.tsx @@ -1,562 +1,307 @@ "use client"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; -import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; import { - TrendingUp, - DollarSign, - CreditCard, - Clock, - CheckCircle2, - ArrowRight, Wallet, - PiggyBank, + User, + MessageCircle, + MapPin, + Phone, + CreditCard, + Shield, Activity, - Landmark, - FileText, - Milestone, - BarChart4, - CircleDollarSign, - BadgeCheck, - ShieldCheck, + CheckCircle2, } from "lucide-react"; import { useDashboard } from "../../hooks/useDashboard.hook"; export function DashboardOverview() { - const { - loading, - stats, - recentActivity, - loanTrends, - formatCurrency, - formatDate, - } = useDashboard(); - - const getActivityIcon = (type: string) => { - switch (type) { - case "loan_created": - return ; - case "loan_approved": - return ; - case "milestone_completed": - return ; - case "payment_received": - return ; - case "loan_completed": - return ; - default: - return ; - } - }; - - const getStatusBadge = (status?: string) => { - if (!status) return null; - - switch (status) { - case "completed": - return ( - - Completed - - ); - case "pending": - return ( - - Pending - - ); - case "rejected": - return ( - - Rejected - - ); - default: - return null; - } - }; + const { loading, profile, address, walletName, chatCount } = useDashboard(); if (loading) { return (
-
- {[...Array(4)].map((_, i) => ( +
+ {[...Array(3)].map((_, i) => ( - + + ))}
- -
- - - - - - - - - - - - - - - {[...Array(5)].map((_, i) => ( -
- -
- - -
-
- ))} -
-
-
); } + const formatAddress = (addr: string) => { + if (!addr) return "Not connected"; + return `${addr.slice(0, 6)}...${addr.slice(-4)}`; + }; + return (
+ {/* Header */}
-

Dashboard

+

+ Dashboard Overview +

- Welcome back! Here is an overview of your loan activity and financial - status. + Your account summary, wallet information, and activity overview.

-
+ {/* Main Cards Grid */} +
+ {/* Wallet Information Card */}
- - Total Loan Volume - - {formatCurrency(stats.totalAmount)} + + +
+ +
+ Wallet Information
- -
-
- - 12% from last month + +
+
+ + + Address + +
+ {address ? ( + <> + + + Connected + + + ) : ( + + Disconnected + + )} +
- - {stats.totalLoans} loans - + +
+

+ {formatAddress(address || "")} +

+
+ + {walletName && ( +
+ + + Wallet Type + + {walletName} +
+ )}
+ {/* User Profile Card */}
- - Active Loans - {stats.activeLoans} + + +
+ +
+ User Profile +
- -
-
- - {formatCurrency(stats.totalAmount * 0.4)} + + {profile ? ( +
+
+ + Full Name + + + {profile.firstName} {profile.lastName} + +
+ +
+ + + Country + + {profile.country} +
+ +
+ + + Phone + + {profile.phoneNumber} +
+ +
+ + + Profile Complete + +
- In progress -
+ ) : ( +
+
+ +
+

+ No profile data available +

+ + Setup Required + +
+ )} + {/* Activity Summary Card */}
- - Available Balance - - {formatCurrency(stats.availableBalance)} + + +
+ +
+ Activity Summary
- -
-
- - Available to withdraw + +
+
+ + + Total Chats + +
+ + {chatCount} + +
- -
-
- - -
- - Pending Approvals - {stats.pendingApprovals} - - -
-
- - Awaiting review +
+
+ Chat Activity + + Active + +
+

+ You have participated in {chatCount} conversation + {chatCount !== 1 ? "s" : ""} on the platform. +

+
+ +
+
+

+ {chatCount > 0 ? "100%" : "0%"} +

+

Engagement

+
+
+

+ {chatCount > 5 ? "High" : chatCount > 0 ? "Medium" : "Low"} +

+

+ Activity Level +

+
-
-
- -
- - - Overview - - - Loans - - - Earnings - - - -
- - - - -
- Financial Overview -
- -
- Loans - - -
- Repayments - -
-
- - -
- {loanTrends.map((data, i) => ( -
-
-
-
-
- - {data.month} - -
- ))} -
- - - - - - Loan Distribution - - -
-
-
- -
- Active Loans - - - {stats.activeLoans} ( - {((stats.activeLoans / stats.totalLoans) * 100).toFixed( - 0, - )} - %) - -
- -
- -
-
- -
- Pending Loans - - - {stats.pendingApprovals} ( - {( - (stats.pendingApprovals / stats.totalLoans) * - 100 - ).toFixed(0)} - %) - -
- -
- -
-
- -
- Completed Loans - - - {stats.completedLoans} ( - {( - (stats.completedLoans / stats.totalLoans) * - 100 - ).toFixed(0)} - %) - -
- -
- -
-
-
- - Total Loans - -

{stats.totalLoans}

-
-
- - Avg. Amount - -

- {formatCurrency(stats.totalAmount / stats.totalLoans)} -

-
-
- - Success Rate - -

- 94% -

-
-
-
+ {/* Additional Info Section */} + {(profile || address) && ( + + + Account Status + + +
+
+
+
+

Wallet Connection

+

+ {address ? "Connected and verified" : "Not connected"} +

- - - - - - Earnings Overview - - -
-
- - -
-
- -
-
-

- Total Earnings -

-

- {formatCurrency(stats.totalAmount * 0.12)} -

-
-
-
-
- - - -
-
- -
-
-

- Interest Earned -

-

- {formatCurrency(stats.totalAmount * 0.08)} -

-
-
-
-
-
- -
-

Earnings Breakdown

-
-
- - - Interest Income - - - {formatCurrency(stats.totalAmount * 0.08)} - -
-
- - - Platform Fees - - - {formatCurrency(stats.totalAmount * 0.03)} - -
-
- - - Milestone Bonuses - - - {formatCurrency(stats.totalAmount * 0.01)} - -
-
-
+
- +
+
+
+

Profile Setup

+

+ {profile + ? "Complete profile information" + : "Profile setup required"} +

- - - - +
-
- - - Recent Activity - - - {recentActivity.slice(0, 4).map((activity) => ( -
-
- {getActivityIcon(activity.type)} -
-
-
-

{activity.title}

- {getStatusBadge(activity.status)} -
-

- {activity.description} -

-
- - {formatDate(activity.date)} - - {activity.amount && ( - - {formatCurrency(activity.amount)} - - )} -
-
+
+
0 ? "bg-emerald-500" : "bg-gray-400"}`} + /> +
+

Platform Activity

+

+ {chatCount > 0 ? "Active participant" : "New to platform"} +

- ))} - - - - - -
-
+
+
+ + + )}
); } diff --git a/src/components/modules/escrows/ui/pages/dashboard.tsx b/src/components/modules/escrows/ui/pages/dashboard.tsx index 80939949..faa7b977 100644 --- a/src/components/modules/escrows/ui/pages/dashboard.tsx +++ b/src/components/modules/escrows/ui/pages/dashboard.tsx @@ -1,12 +1,6 @@ "use client"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { useWalletContext } from "@/providers/wallet.provider"; import { MainTabs } from "../tabs/MainTabs"; import { ConnectWalletWarning } from "../ConnectWalletWarning"; @@ -17,14 +11,19 @@ export function Loans() { return (
- - Loans - + {/* Nuevo Header con texto anterior */} +
+
+
+

Loans

+
+

Manage escrow contracts and interact with the Stellar blockchain using the Trustless Work API. - - - +

+
+ + {walletAddress ? : } diff --git a/src/components/modules/maintenance/hooks/useCountdown.ts b/src/components/modules/maintenance/hooks/useCountdown.ts new file mode 100644 index 00000000..6a6ac1af --- /dev/null +++ b/src/components/modules/maintenance/hooks/useCountdown.ts @@ -0,0 +1,46 @@ +import { useState, useEffect } from "react"; +interface Countdown { + hours: number; + minutes: number; +} + +const useCountdown = (initialHours: number, initialMinutes: number) => { + const hours = + initialHours || + parseInt(process.env.NEXT_PUBLIC_COUNTDOWN_HOURS || "2", 10); + const minutes = + initialMinutes || + parseInt(process.env.NEXT_PUBLIC_COUNTDOWN_MINUTES || "30", 10); + + const [time, setTime] = useState({ + hours, + minutes, + }); + + useEffect(() => { + const timer = setInterval(() => { + setTime((prev) => { + if (prev.hours === 0 && prev.minutes === 0) { + clearInterval(timer); + return prev; + } + + let { hours, minutes } = prev; + minutes -= 1; + + if (minutes < 0) { + minutes = 59; + hours -= 1; + } + + return { hours, minutes }; + }); + }, 60000); + + return () => clearInterval(timer); + }, []); + + return time; +}; + +export default useCountdown; diff --git a/src/components/modules/maintenance/ui/CountdownTimer.tsx b/src/components/modules/maintenance/ui/CountdownTimer.tsx new file mode 100644 index 00000000..22904574 --- /dev/null +++ b/src/components/modules/maintenance/ui/CountdownTimer.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Card } from "@/components/ui/card"; +import { Clock } from "lucide-react"; +import useCountdown from "../hooks/useCountdown"; + +const CountdownTimer = () => { + const hours = parseInt(process.env.NEXT_PUBLIC_COUNTDOWN_HOURS || "0", 10); + const minutes = parseInt( + process.env.NEXT_PUBLIC_COUNTDOWN_MINUTES || "0", + 10, + ); + + const remainingTime = useCountdown(hours, minutes); + + return ( + +
+ +

Estimated Time

+
+
+ {["Hours", "Minutes"].map((label, index) => { + const timeValue = [remainingTime.hours, remainingTime.minutes][index]; + return ( +
+
+ {timeValue.toString().padStart(2, "0")} +
+
+ {label.toUpperCase()} +
+
+ ); + })} +
+
+ ); +}; + +export default CountdownTimer; diff --git a/src/components/modules/marketplace/hooks/marketplace.hook.ts b/src/components/modules/marketplace/hooks/marketplace.hook.ts new file mode 100644 index 00000000..486daa4f --- /dev/null +++ b/src/components/modules/marketplace/hooks/marketplace.hook.ts @@ -0,0 +1,161 @@ +"use client"; + +import { useState } from "react"; + +export interface LoanOffer { + id: number; + lender: string; + amount: number; + interestRate: number; + termMonths: number; + status?: "available" | "pending" | "funded"; + createdAt?: string; +} + +const sampleLoans: LoanOffer[] = [ + { + id: 1, + lender: "Alice", + amount: 500, + interestRate: 5, + termMonths: 6, + status: "available", + createdAt: "2024-01-15", + }, + { + id: 2, + lender: "Bob", + amount: 1000, + interestRate: 7, + termMonths: 12, + status: "available", + createdAt: "2024-01-14", + }, + { + id: 3, + lender: "Charlie", + amount: 750, + interestRate: 6, + termMonths: 9, + status: "pending", + createdAt: "2024-01-13", + }, +]; + +export function useMarketplace() { + const [loans, setLoans] = useState(sampleLoans); + const [showForm, setShowForm] = useState(false); + const [amount, setAmount] = useState(""); + const [interest, setInterest] = useState(""); + const [term, setTerm] = useState(""); + + const addLoan = () => { + if (!amount || !interest || !term) return; + + const newLoan: LoanOffer = { + id: loans.length + 1, + lender: "You", + amount: Number(amount), + interestRate: Number(interest), + termMonths: Number(term), + status: "available", + createdAt: new Date().toISOString().split("T")[0], + }; + + setLoans([...loans, newLoan]); + setAmount(""); + setInterest(""); + setTerm(""); + setShowForm(false); + }; + + const toggleForm = () => { + setShowForm(!showForm); + }; + + const getStatusInfo = (status?: string) => { + switch (status) { + case "available": + return { + label: "Available", + variant: "outline" as const, + className: "bg-emerald-50 text-emerald-700 border-emerald-200", + }; + case "pending": + return { + label: "Pending", + variant: "outline" as const, + className: "bg-amber-50 text-amber-700 border-amber-200", + }; + case "funded": + return { + label: "Funded", + variant: "outline" as const, + className: "bg-blue-50 text-blue-700 border-blue-200", + }; + default: + return null; + } + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + }; + + const calculateMonthlyPayment = ( + amount: number, + interestRate: number, + termMonths: number, + ) => { + return (amount * (1 + interestRate / 100)) / termMonths; + }; + + const calculateTotalRepayment = (amount: number, interestRate: number) => { + return amount * (1 + interestRate / 100); + }; + + // Computed values + const availableLoans = loans.filter((loan) => loan.status === "available"); + const totalVolume = loans.reduce((sum, loan) => sum + loan.amount, 0); + const avgInterest = + loans.length > 0 + ? loans.reduce((sum, loan) => sum + loan.interestRate, 0) / loans.length + : 0; + + const stats = { + totalVolume, + availableLoans: availableLoans.length, + avgInterest, + totalOffers: loans.length, + availableVolume: availableLoans.reduce((sum, loan) => sum + loan.amount, 0), + }; + + return { + // State + loans, + showForm, + amount, + interest, + term, + + // Actions + addLoan, + toggleForm, + setAmount, + setInterest, + setTerm, + + // Utilities + getStatusInfo, // Cambiar de getStatusBadge a getStatusInfo + formatCurrency, + calculateMonthlyPayment, + calculateTotalRepayment, + + // Computed values + availableLoans, + stats, + }; +} diff --git a/src/components/modules/marketplace/ui/pages/MarketplacePage.tsx b/src/components/modules/marketplace/ui/pages/MarketplacePage.tsx new file mode 100644 index 00000000..021d7f47 --- /dev/null +++ b/src/components/modules/marketplace/ui/pages/MarketplacePage.tsx @@ -0,0 +1,278 @@ +"use client"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Plus, + DollarSign, + Calendar, + Percent, + User, + Clock, + ArrowRight, +} from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { useMarketplace } from "../../hooks/marketplace.hook"; + +export function MarketplacePage() { + const { + loans, + showForm, + amount, + interest, + term, + addLoan, + toggleForm, + setAmount, + setInterest, + setTerm, + getStatusInfo, + formatCurrency, + calculateMonthlyPayment, + calculateTotalRepayment, + } = useMarketplace(); + + const getStatusBadge = (status?: string) => { + const statusInfo = getStatusInfo(status); + if (!statusInfo) return null; + + return ( + + {statusInfo.label} + + ); + }; + + return ( +
+ {/* Header */} +
+
+
+

+ Marketplace +

+
+

+ Discover loan opportunities or offer your own. Connect with borrowers + and lenders in our secure marketplace. +

+
+ + {/* Main Content */} + + +
+
+ Loan Offers + + Browse available loan offers or create your own to attract + borrowers. + +
+ +
+
+ + + {/* Loan Creation Form */} + {showForm && ( + + + + + Create Loan Offer + + + Set your terms and attract potential borrowers to your offer. + + + +
+
+ + setAmount(e.target.value)} + className="bg-white dark:bg-gray-950 h-11" + /> +
+
+
+ + setInterest(e.target.value)} + className="bg-white dark:bg-gray-950 h-11" + /> +
+
+ + setTerm(e.target.value)} + className="bg-white dark:bg-gray-950 h-11" + /> +
+
+
+
+ +
+
+
+ )} + + {/* Loan Offers Grid */} +
+ {loans.map((loan) => ( + +
+ +
+ + + {loan.lender} + + {getStatusBadge(loan.status)} +
+
+
+ + {formatCurrency(loan.amount)} + + + {loan.interestRate}% + +
+
+ + + {loan.termMonths}mo + + + + {loan.createdAt} + +
+
+
+ +
+
+

+ Monthly:{" "} + + {formatCurrency( + calculateMonthlyPayment( + loan.amount, + loan.interestRate, + loan.termMonths, + ), + )} + +

+

+ Total:{" "} + + {formatCurrency( + calculateTotalRepayment( + loan.amount, + loan.interestRate, + ), + )} + +

+
+ +
+
+ + ))} +
+ + {loans.length === 0 && ( +
+
+ +
+

+ No loan offers yet +

+

+ Be the first to create a loan offer in the marketplace. +

+ +
+ )} + +
+
+ ); +} diff --git a/src/components/modules/profile/schemas/profile.schema.ts b/src/components/modules/profile/schemas/profile.schema.ts new file mode 100644 index 00000000..87e6f4df --- /dev/null +++ b/src/components/modules/profile/schemas/profile.schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const profileSchema = z.object({ + firstName: z.string().min(1, "First name is required"), + lastName: z.string().min(1, "Last name is required"), + country: z.string().min(1, "Country is required"), + phoneNumber: z.string().min(1, "Phone number is required"), + walletAddress: z.string().min(1, "Wallet address is required"), +}); diff --git a/src/components/modules/profile/ui/UserProfileForm.tsx b/src/components/modules/profile/ui/UserProfileForm.tsx new file mode 100644 index 00000000..5f6492ec --- /dev/null +++ b/src/components/modules/profile/ui/UserProfileForm.tsx @@ -0,0 +1,276 @@ +"use client"; + +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useUserContext } from "@/providers/user.provider"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Loader2, User, MapPin, Phone, Wallet } from "lucide-react"; +import { profileSchema } from "../schemas/profile.schema"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { useWalletContext } from "@/providers/wallet.provider"; + +type FormValues = z.infer; + +export const UserProfileForm = () => { + const { profile, loading, saving, saveProfile } = useUserContext(); + const { walletAddress } = useWalletContext(); + + const form = useForm({ + resolver: zodResolver(profileSchema), + defaultValues: { + firstName: profile?.firstName || "", + lastName: profile?.lastName || "", + country: profile?.country || "", + phoneNumber: profile?.phoneNumber || "", + walletAddress: walletAddress || "", + }, + }); + + // Update form values when profile loads + React.useEffect(() => { + if (profile) { + form.reset({ + firstName: profile.firstName || "", + lastName: profile.lastName || "", + country: profile.country || "", + phoneNumber: profile.phoneNumber || "", + walletAddress: walletAddress || "", + }); + } + }, [profile, form]); + + const onSubmit = async (data: FormValues) => { + await saveProfile(data); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + {/* Progress Bar */} +
+
+
+ +

User Registration

+

+ Complete the information to create your profile on the platform +

+
+ + +
+ + {/* First Name and Last Name */} +
+ ( + + + + First Name + + + + + + + )} + /> + + ( + + + + Last Name + + + + + + + )} + /> +
+ + {/* Country */} + ( + + + + Country + + + + + )} + /> + + {/* Phone Number */} + ( + + + + Phone Number + + + + + + + )} + /> + + {/* Wallet */} + ( + + + + Wallet + + + + +

+ Your digital wallet address for transactions +

+ +
+ )} + /> + + {/* Submit Button */} + + + +
+
+
+
+ ); +}; diff --git a/src/config/wallet-kit.ts b/src/config/wallet-kit.ts index 137fd550..076abb8b 100644 --- a/src/config/wallet-kit.ts +++ b/src/config/wallet-kit.ts @@ -3,7 +3,13 @@ import { WalletNetwork, FREIGHTER_ID, FreighterModule, + AlbedoModule, + xBullModule, + LobstrModule, + RabetModule, + allowAllModules, } from "@creit.tech/stellar-wallets-kit"; +import { setAllowedWallets } from "@creit.tech/stellar-wallets-kit/state/store"; /** * @@ -14,5 +20,22 @@ import { export const kit: StellarWalletsKit = new StellarWalletsKit({ network: WalletNetwork.TESTNET, selectedWalletId: FREIGHTER_ID, - modules: [new FreighterModule()], + modules: + process.env.NODE_ENV !== "production" + ? allowAllModules() + : [ + new FreighterModule(), + new AlbedoModule(), + new xBullModule(), + new LobstrModule(), + new RabetModule(), + ], +}); + +// Force Lobstr and Rabet modules to be marked as available +kit.getSupportedWallets().then((wallets) => { + const updated = wallets.map((w) => + w.id === "lobstr" || w.id === "rabet" ? { ...w, isAvailable: true } : w, + ); + setAllowedWallets(updated); }); diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts new file mode 100644 index 00000000..30bcecf5 --- /dev/null +++ b/src/lib/firebase.ts @@ -0,0 +1,15 @@ +// lib/firebase.ts +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, +}; + +const app = initializeApp(firebaseConfig); +export const db = getFirestore(app); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a5ef1935..d5ac3a77 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,11 @@ -import { clsx, type ClassValue } from "clsx"; +import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function formatAddress(address: string) { + if (!address) return ""; + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} diff --git a/src/providers/global.provider.tsx b/src/providers/global.provider.tsx index 7c203f34..21f5924e 100644 --- a/src/providers/global.provider.tsx +++ b/src/providers/global.provider.tsx @@ -4,13 +4,16 @@ import { EscrowProvider } from "./escrow.provider"; import { WalletProvider } from "./wallet.provider"; import { TrustlessWorkProvider } from "./trustless-work.provider"; import { TabsProvider } from "./tabs.provider"; +import { UserProvider } from "./user.provider"; export const GlobalProvider = ({ children }: { children: React.ReactNode }) => { return ( - {children} + + {children} + diff --git a/src/providers/user.provider.tsx b/src/providers/user.provider.tsx new file mode 100644 index 00000000..1928a1d6 --- /dev/null +++ b/src/providers/user.provider.tsx @@ -0,0 +1,101 @@ +import React, { createContext, useContext, useState, useEffect } from "react"; +import { db } from "@/lib/firebase"; +import { doc, getDoc, setDoc } from "firebase/firestore"; +import { UserProfile, UserProfileFormData } from "@/@types/user.entity"; +import { useWalletContext } from "@/providers/wallet.provider"; +import { toast } from "sonner"; + +interface UserContextType { + profile: UserProfile | null; + loading: boolean; + saving: boolean; + saveProfile: (data: UserProfileFormData) => Promise; +} + +const UserContext = createContext(undefined); + +export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { walletAddress } = useWalletContext(); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (walletAddress) { + loadProfile(); + } else { + setProfile(null); + setLoading(false); + } + }, [walletAddress]); + + const loadProfile = async () => { + if (!walletAddress) return; + + try { + setLoading(true); + const userDoc = await getDoc(doc(db, "users", walletAddress)); + + if (userDoc.exists()) { + setProfile(userDoc.data() as UserProfile); + } else { + setProfile(null); + } + } catch (error) { + console.error("Error loading user profile:", error); + toast.error("Failed to load profile"); + } finally { + setLoading(false); + } + }; + + const saveProfile = async (data: UserProfileFormData) => { + if (!walletAddress) { + toast.error("Please connect your wallet first"); + return; + } + + try { + setSaving(true); + const now = Date.now(); + const userData: UserProfile = { + walletAddress, + ...data, + createdAt: profile?.createdAt || now, + updatedAt: now, + }; + + await setDoc(doc(db, "users", walletAddress), userData); + setProfile(userData); + toast.success("Profile saved successfully"); + } catch (error) { + console.error("Error saving user profile:", error); + toast.error("Failed to save profile"); + } finally { + setSaving(false); + } + }; + + return ( + + {children} + + ); +}; + +export const useUserContext = () => { + const context = useContext(UserContext); + if (context === undefined) { + throw new Error("useUserContext must be used within a UserProvider"); + } + return context; +}; diff --git a/src/utils/theme-toggle.tsx b/src/utils/theme-toggle.tsx deleted file mode 100644 index 1cb6a776..00000000 --- a/src/utils/theme-toggle.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; -import { useTheme } from "next-themes"; -import { Moon, Sun } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -export function ThemeToggle() { - const { setTheme, theme } = useTheme(); - - return ( - - - - - - setTheme("light")} - className={theme === "light" ? "bg-accent" : ""} - > - Light - - setTheme("dark")} - className={theme === "dark" ? "bg-accent" : ""} - > - Dark - - setTheme("system")} - className={theme === "system" ? "bg-accent" : ""} - > - System - - - - ); -}