diff --git a/app/ThemeContext.tsx b/app/ThemeContext.tsx deleted file mode 100644 index bd528df..0000000 --- a/app/ThemeContext.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' -import { createContext, useContext, useState, ReactNode, useEffect } from 'react'; - -interface ThemeContextType { - isDarkMode: boolean; - toggleDarkMode: () => void; -} - -const ThemeContext = createContext(undefined); - -export function ThemeProvider({ children }: { children: ReactNode }) { - const [isDarkMode, setIsDarkMode] = useState(true); - - useEffect(() => { - const savedTheme = localStorage.getItem("theme"); - if (savedTheme) { - setIsDarkMode(savedTheme === "dark"); - } - }, []); - - const toggleDarkMode = () => { - setIsDarkMode((prev) => { - const newMode = !prev; - localStorage.setItem("theme", newMode ? "dark" : "light"); - return newMode; - }); - }; - - return ( - - {children} - - ); -} - -export function useTheme() { - const context = useContext(ThemeContext); - if (context === undefined) { - throw new Error('useTheme must be used within a ThemeProvider'); - } - return context; -} \ No newline at end of file diff --git a/app/about/page.tsx b/app/about/page.tsx deleted file mode 100644 index 95903f2..0000000 --- a/app/about/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client' -import React from 'react'; -import AboutTech from '../components/about-technology'; -import AboutTeam from '../components/about-team'; -import AboutCoreProblems from "../components/about-core-problems"; -import HomeNav from '../components/HomeNav'; -import Footer from '../components/footer'; -import AboutHeader from '../components/about-header'; - -const AboutPage = () => { - return ( -
- -
- - - - -
-
-
- ); -}; - -export default AboutPage; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..85046eb --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,165 @@ +import NextAuth from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { ethers } from "ethers"; +import { SiweMessage } from "siwe"; + +const handler = NextAuth({ + providers: [ + CredentialsProvider({ + id: "ethereum", + name: "Ethereum", + credentials: { + address: { label: "Address", type: "text" }, + signature: { label: "Signature", type: "text" }, + message: { label: "Message (SIWE)", type: "text" }, + nonce: { label: "Nonce", type: "text" }, + }, + async authorize(credentials, req) { + if ( + !credentials?.address || + !credentials?.signature || + !credentials?.message || + !credentials?.nonce + ) { + return null; + } + + try { + // Check if the address is an Ethereum address + const isEthAddress = + credentials.address.startsWith("0x") && + credentials.address.length === 42; + + // Check if the address is a Starknet address (starts with 0x and is 64 or 66 chars) + const isStarknetAddress = + credentials.address.startsWith("0x") && + (credentials.address.length === 66 || + credentials.address.length === 64); + + if (isEthAddress) { + // Process Ethereum wallet authentication + + // Input validation guards + // 1. Verify credentials.address is a valid EVM address + if (!ethers.isAddress(credentials.address)) { + console.error("Invalid EVM address format"); + return null; + } + + // 2. Ensure credentials.signature is a 65-byte hex string (0x-prefixed, 132 characters) + const signatureRegex = /^0x[a-fA-F0-9]{130}$/; + if (!signatureRegex.test(credentials.signature)) { + console.error( + "Invalid signature format - must be 65-byte hex string" + ); + return null; + } + + // 3. Enforce sane credentials.message length limit (max 1024 chars) + if (credentials.message.length > 1024) { + console.error("Message too long - exceeds 1024 character limit"); + return null; + } + + // Parse and verify SIWE message + const siwe = new SiweMessage(credentials.message); + const domain = new URL( + process.env.NEXTAUTH_URL ?? req.headers?.origin ?? "" + ).host; + + // Check message timing to prevent replay attacks + const now = new Date(); + if (siwe.expirationTime && new Date(siwe.expirationTime) < now) { + console.error("SIWE message has expired"); + return null; + } + if (siwe.issuedAt) { + const issuedAt = new Date(siwe.issuedAt); + const maxAge = 5 * 60 * 1000; // 5 minutes + if (now.getTime() - issuedAt.getTime() > maxAge) { + console.error("SIWE message is too old"); + return null; + } + } + + // Validate the signature and message fields + await siwe.verify({ + signature: credentials.signature, + domain, + time: new Date().toISOString(), + }); + + // Compare recovered address with the provided one (canonicalize) + const recovered = ethers.getAddress(siwe.address); + const provided = ethers.getAddress(credentials.address); + if (recovered !== provided) { + console.error( + "Address mismatch between SIWE message and provided address" + ); + return null; + } + + return { + id: recovered, + name: recovered, + address: recovered, + }; + } else if (isStarknetAddress) { + // Process Starknet wallet authentication + + // For Starknet, we simply verify the message contains our expected format + // and trust the signature verification done by the wallet + if ( + !credentials.message.includes( + "Sign this message to authenticate with ZeroXBridge" + ) + ) { + console.error( + "Invalid message format for Starknet authentication" + ); + return null; + } + + // For now, we'll simply authenticate Starknet users by their address + // In production, you'd want to implement proper Starknet signature verification + return { + id: credentials.address, + name: credentials.address, + address: credentials.address, + }; + } else { + console.error("Invalid address format - neither ETH nor Starknet"); + return null; + } + } catch (error) { + console.error("Signature verification failed:", error); + return null; + } + }, + }), + ], + session: { + strategy: "jwt", + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.address = user.address; + } + return token; + }, + async session({ session, token }) { + if (token) { + session.user.address = token.address as string; + } + return session; + }, + }, + pages: { + signIn: "/auth/signin", + error: "/auth/error", + }, + secret: process.env.NEXTAUTH_SECRET, +}); + +export { handler as GET, handler as POST }; diff --git a/app/api/auth/csrf/route.ts b/app/api/auth/csrf/route.ts new file mode 100644 index 0000000..e647101 --- /dev/null +++ b/app/api/auth/csrf/route.ts @@ -0,0 +1,32 @@ +import { NextRequest } from "next/server"; +import { getCsrfToken } from "next-auth/react"; + +export async function GET(req: NextRequest) { + const csrfToken = await getCsrfToken(); + + if (!csrfToken) { + return new Response( + JSON.stringify({ + error: "Failed to generate CSRF token", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + return new Response( + JSON.stringify({ + csrfToken, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); +} diff --git a/app/components/Ethereum-provider.tsx b/app/components/Ethereum-provider.tsx deleted file mode 100644 index bf9e3f3..0000000 --- a/app/components/Ethereum-provider.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { createContext, useContext, useEffect, useState } from "react"; -import { ethers } from "ethers"; - -interface EthereumContextType { - provider: ethers.BrowserProvider | null; - signer: ethers.JsonRpcSigner | null; - isConnected: boolean; - connectWallet: () => Promise; - address: string | null; -} - -const EthereumContext = createContext({ - provider: null, - signer: null, - isConnected: false, - connectWallet: async () => {}, - address: null, -}); - -export const EthereumProvider = ({ children }: { children: React.ReactNode }) => { - const [provider, setProvider] = useState(null); - const [signer, setSigner] = useState(null); - const [isConnected, setIsConnected] = useState(false); - const [address, setAddress] = useState(null); - - const connectWallet = async () => { - try { - if (typeof window.ethereum === "undefined") { - throw new Error("Please install MetaMask"); - } - - const provider = new ethers.BrowserProvider(window.ethereum); - await provider.send("eth_requestAccounts", []); - const signer = await provider.getSigner(); - const address = await signer.getAddress(); - - setProvider(provider); - setSigner(signer); - setAddress(address); - setIsConnected(true); - } catch (error) { - console.error("Error connecting wallet:", error); - } - }; - - useEffect(() => { - const checkConnection = async () => { - if (typeof window.ethereum !== "undefined") { - const provider = new ethers.BrowserProvider(window.ethereum); - const accounts = await provider.send("eth_accounts", []); - - if (accounts.length > 0) { - const signer = await provider.getSigner(); - const address = await signer.getAddress(); - - setProvider(provider); - setSigner(signer); - setAddress(address); - setIsConnected(true); - } - } - }; - - checkConnection(); - - // Listen for account changes - if (window.ethereum) { - window.ethereum.on("accountsChanged", () => { - checkConnection(); - }); - } - - return () => { - if (window.ethereum) { - window.ethereum.removeListener("accountsChanged", checkConnection); - } - }; - }, []); - - return ( - - {children} - - ); -}; - -export const useEthereum = () => { - const context = useContext(EthereumContext); - if (context === undefined) { - throw new Error("useEthereum must be used within an EthereumProvider"); - } - return context; -}; \ No newline at end of file diff --git a/app/components/FAQ.tsx b/app/components/FAQ.tsx deleted file mode 100644 index 404a724..0000000 --- a/app/components/FAQ.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import { Manrope, Roboto_Serif } from "next/font/google"; -import Image from "next/image"; -import React, { useState } from "react"; - -interface FaqItem { - item: { - question: string; - answer: string; - }; - onToggle: () => void; - isOpen: boolean; -} -const manrope = Manrope({ - subsets: ["latin"], - weight: ["400", "600", "700"], -}); -const roboto = Roboto_Serif({ - subsets: ["latin"], - weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], -}); -const FaqItem = ({ item, onToggle, isOpen }: FaqItem) => { - return ( -
-
-

- {item.question} -

- - arrowdown -
- -
-

- {item.answer} -

-
-
- ); -}; - -const FAQ = () => { - const [isOpenIndex, setIsOpenIndex] = useState(null); - const onToggle = (index: number | null) => { - setIsOpenIndex(isOpenIndex === index ? null : index); - }; - return ( -
-
-

- You have a Question You’re not clear about? -

-

- ZeroXBridge is here to answer all your questions and keep you updated. -

- - {Array(7) - .fill(null) - .map((_, index) => { - return ( - onToggle(index)} - isOpen={index === isOpenIndex} - /> - ); - })} - -
- -
-
- ); -}; - -export default FAQ; diff --git a/app/components/HomeNav.tsx b/app/components/HomeNav.tsx deleted file mode 100644 index bbdb688..0000000 --- a/app/components/HomeNav.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; -import { useState } from "react"; -import Link from "next/link"; -import { Button } from "./ui/button"; -import Image from "next/image"; - - -type NavLink = { - name: string; - href: string; -}; - -const navLinks: NavLink[] = [ - { name: "About Us", href: "/about" }, - { name: "Pricing", href: "#" }, - { name: "Features", href: "#" }, - { name: "Contact Us", href: "#" }, -]; - -const HomeNav = () => { - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const toggleMenu = () => { - setIsMenuOpen(!isMenuOpen); - }; - - return ( -
- -
- ); -}; - -export default HomeNav; diff --git a/app/components/LanguageSwitcher.tsx b/app/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..a2cc6d3 --- /dev/null +++ b/app/components/LanguageSwitcher.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; +import { useLanguage } from "../contexts/LanguageContext"; +import { useTranslation } from "react-i18next"; +import { Globe } from "lucide-react"; +import { useThemeContext } from "../context/theme-provider"; +import "../i18n-client"; + +interface LanguageSwitcherProps { + className?: string; + size?: "sm" | "md" | "lg"; + showText?: boolean; +} + +const LanguageSwitcher: React.FC = ({ + className = "", + size = "md", + showText = true, +}) => { + const { t } = useTranslation(); + const { currentLanguage, changeLanguage, isLoading } = useLanguage(); + const { isDark } = useThemeContext(); + + const handleLanguageChange = async () => { + const newLanguage = currentLanguage === "en" ? "fr" : "en"; + await changeLanguage(newLanguage); + }; + + const sizeClasses = { + sm: "px-2 py-1 text-xs", + md: "px-3 py-2 text-sm", + lg: "px-4 py-3 text-base", + }; + + const iconSizes = { + sm: 14, + md: 16, + lg: 18, + }; + + return ( + + ); +}; + +export default LanguageSwitcher; diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx deleted file mode 100644 index 03f0242..0000000 --- a/app/components/Sidebar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; -import { useState } from "react"; -import Image from "next/image"; -import Chart from "../../public/Chart.png"; -import Swap from "../../public/Swap.png"; -import Claim from "../../public/Claim.png"; -import Coins from "../../public/Coins.png"; -import Dashboard from "../../public/dashboard.png"; -import { cn } from "@/utils/cn"; -import { useTheme } from '../ThemeContext'; -import { Settings } from "lucide-react"; -import Link from "next/link"; - -const Sidebar = () => { - const { isDarkMode } = useTheme(); - const [activeTab, setActiveTab] = useState("Dashboard"); - - return ( - - ); -}; - -export default Sidebar; \ No newline at end of file diff --git a/app/components/TradingChart/Coins.tsx b/app/components/TradingChart/Coins.tsx deleted file mode 100644 index a499df4..0000000 --- a/app/components/TradingChart/Coins.tsx +++ /dev/null @@ -1,146 +0,0 @@ -export const BTCIcon = () => { - return ( - - - - - ) -} - -export const BCHIcon = () => { - return ( - - - - - - ) -} - -export const ETHIcon = () => { - return ( - - - - - ) -} - -export const LTCIcon = () => { - return ( - - - - ) -} - -export const ETCIcon = () => { - return ( - - - - - ) -} - -export const XRPIcon = () => { - return ( - - - - - ) -} - -export const FCTIcon = () => { - return ( - - - - - ) -} - -export const LSKIcon = () => { - return ( - - - - - ) -} - -export const XEMIcon = () => { - return ( - - - - - ) -} - -export const USKIcon = () => { - return ( - - - - - ) -} - -export const FilterIcon = () => { - return ( - - - - - ) -} - -export const DownArrow = () => { - return ( - - - - ) -} - -export const UpArrow = () => { - return ( - - - - ) -} - -export const NotificationIcon = () => { - return ( - - - - - - - - - - - - ) -} - -export const DetailsIcon = () => { - return ( - - - - ) -} \ No newline at end of file diff --git a/app/components/TradingChart/chart.tsx b/app/components/TradingChart/chart.tsx deleted file mode 100644 index a559581..0000000 --- a/app/components/TradingChart/chart.tsx +++ /dev/null @@ -1,171 +0,0 @@ -'use client' - -import { CandlestickData, CandlestickSeries, ColorType, createChart, HistogramData, HistogramSeries, IChartApi, Time } from "lightweight-charts" -import React, { useEffect, useRef, useState } from "react" -import { DetailsIcon } from "./Coins" - -type ChartProps = { - selectedInterval: { - name: string, secondsValue: number - }, - isDarkMode: boolean -} - -const toTime = (timestamp: number): Time => timestamp as Time - -const generateMockData = () => { - const intervalInSeconds = 300; // 5 minutes - const numberOfPoints = 30 * 24 * 12; - const startTime = Date.now() / 1000 - numberOfPoints * intervalInSeconds; - const data: CandlestickData[] = []; - let previousClose = 720000; - - for (let i = 0; i < numberOfPoints; i++) { - const time = toTime(Math.floor(startTime + i * intervalInSeconds)); - const open = previousClose; - const close = open + (Math.random() - 0.5) * 2000; - const high = Math.max(open, close) + Math.random() * 1000; - const low = Math.min(open, close) - Math.random() * 1000; - - data.push({ time, open, high, low, close }); - previousClose = close; - } - return data; -}; - -export default function Chart({ selectedInterval, isDarkMode }: ChartProps) { - const chartContainerRef = useRef(null) - - const chartRef = useRef(null) - const [scrollPos, setScrollPos] = useState(0) - const [dataLength, setDataLength] = useState(0) - - useEffect(() => { - - const candleStickData = generateMockData() - setDataLength(candleStickData.length) - - const volumeData: HistogramData[] = candleStickData.map(candle => ({ - time: candle.time, - value: Math.floor(Math.random() * 100) + 30, - color: '#EFF2FC' - })) - - if (!chartContainerRef.current) return - - const chart = createChart(chartContainerRef.current, { - layout: { - background: { type: ColorType.Solid, color: 'transparent' }, - textColor: '#808080' - }, - width: chartContainerRef.current.clientWidth - 10, - height: 440, - timeScale: { - timeVisible: true, - barSpacing: 16, - borderVisible: false, - rightOffset: -4, - fixLeftEdge: true, - lockVisibleTimeRangeOnResize: true - }, - grid: { - vertLines: { visible: false }, - horzLines: { visible: true } - } - }) - - chartRef.current = chart - - const candleSeries = chart.addSeries(CandlestickSeries, { - upColor: '#CCB7FF', - downColor: '#8280FF', - borderVisible: false, - wickUpColor: '#CCB7FF', - wickDownColor: '#8280FF' - }) - - const volumeSeries = chart.addSeries(HistogramSeries, { - color: '#EFF2FC', - priceFormat: { type: 'volume' }, - priceScaleId: 'volume', // Give volume its own scale - priceLineVisible: false - }) - - // Configure the main price scale (candlesticks) - chart.priceScale('right').applyOptions({ - scaleMargins: { - top: 0.1, // Leave space at the top - bottom: 0.2 // Leave room for volume - }, - borderVisible: false - }) - - // Configure the volume scale - chart.priceScale('volume').applyOptions({ - scaleMargins: { - top: 0.9, // Position volume at the bottom - bottom: 0 // Align with bottom of chart - }, - borderVisible: false, - visible: false // Hide the volume scale - }) - - const secondsToHours = (secondsValue: number) => { - const hoursValue = secondsValue / 3600 - return hoursValue - } - - chart?.timeScale()?.setVisibleLogicalRange({ - from: candleStickData.length - (12 * (9 * secondsToHours(selectedInterval.secondsValue)) + 3), - to: candleStickData.length - }) - - candleSeries.setData(candleStickData) - volumeSeries.setData(volumeData) - - const handleRangeChange = () => { - const currentPos = chart.timeScale().scrollPosition() - setScrollPos(currentPos) - } - chart.timeScale().subscribeVisibleLogicalRangeChange(handleRangeChange) - - return () => { - chart.timeScale().unsubscribeVisibleLogicalRangeChange(handleRangeChange); - chart.remove(); - } - }, [selectedInterval]) - - const handleScrollChange = (e: React.ChangeEvent) => { - const newPos = Number(e.target.value) - setScrollPos(newPos) - - chartRef.current?.timeScale().scrollToPosition(newPos, false) - } - - return ( -
- {/* Chart Container */} -
-
- - {/* 'Scroll bar' under the chart */} -
-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/components/TradingChart/data.ts b/app/components/TradingChart/data.ts deleted file mode 100644 index 267474b..0000000 --- a/app/components/TradingChart/data.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { JSX } from "react"; -import { BCHIcon, BTCIcon, ETCIcon, ETHIcon, FCTIcon, LSKIcon, LTCIcon, USKIcon, XEMIcon, XRPIcon } from "./Coins"; - -type CoinData = { - name: string, icon: () => JSX.Element, price: string, performance: number -} - -export const coinData: CoinData[] = [ - { - name: 'BTC', - icon: BTCIcon, - price: '721,882', - performance: -4.66 - }, - { - name: 'BCH', - icon: BCHIcon, - price: '48,782', - performance: +0.66 - }, - { - name: 'ETH', - icon: ETHIcon, - price: '22,882', - performance: -4.66 - }, - { - name: 'LTC', - icon: LTCIcon, - price: '1,882', - performance: +0.66 - }, - { - name: 'ETC', - icon: ETCIcon, - price: '721,882', - performance: -4.66 - }, - { - name: 'XRP', - icon: XRPIcon, - price: '721,882', - performance: -4.66 - }, - { - name: 'FCT', - icon: FCTIcon, - price: '721,882', - performance: +0.66 - }, - { - name: 'LSK', - icon: LSKIcon, - price: '721,882', - performance: -4.66 - }, - { - name: 'XEM', - icon: XEMIcon, - price: '721,882', - performance: -4.66 - }, - { - name: 'XEM', - icon: XEMIcon, - price: '721,882', - performance: -4.66 - }, - { - name: 'USK', - icon: USKIcon, - price: '721,882', - performance: +0.66 - }, -] \ No newline at end of file diff --git a/app/components/TradingChart/trading-chart.tsx b/app/components/TradingChart/trading-chart.tsx deleted file mode 100644 index 547bfee..0000000 --- a/app/components/TradingChart/trading-chart.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client' - -import { useState } from "react"; -import Chart from "./chart"; -import { DownArrow, FilterIcon, NotificationIcon, UpArrow } from "./Coins"; -import { coinData } from "./data"; - -type Interval = { - name: string, secondsValue: number -} - -export default function TradingChartComponent({ isDarkMode }: { isDarkMode: boolean }){ - - const intervals: Interval[] = [ - { name: '1min', secondsValue: 60}, - { name: '5min', secondsValue: 60 * 5 }, - { name: '15min', secondsValue: 60 * 15 }, - { name: '1 hr', secondsValue: 60 * 60 }, - { name: '4 hr', secondsValue: 60 * 60 * 4 } - ] - - const [selectedInterval, setSelectedInterval] = useState(intervals[3]) - - - return ( -
-
-
-

- BTC - /IPY - -

- -
-
-

- 721,882 - -4 - -

-

- High - 725,974 -

-

- 24h Volume - 677.7 BTC -

-

- Price Alert - -

-
-
-
- {/*

1min

-

5min

-

15min

-

1 hr

-

4 hr

*/} - { - intervals.map((int, idx) => ( -

setSelectedInterval(int)} - > - {int.name} -

- )) - } -
-
- -
-
-
- -
-
-

- - - - - - - - - - Market Cap -

- -
- -
- - { - coinData.map((coinObject, index) => { - const Icon = coinObject.icon - return ( -
-
- - {coinObject.name} -
-

- - ¥ {coinObject.price} - - - - {coinObject.performance < 0 ? `${coinObject.performance}`:`+${coinObject.performance}`} - - - {coinObject.performance < 0 ? ( - - ) : ( - - )} - - -

-
- ) - }) - } -
-
-
- ) -} \ No newline at end of file diff --git a/app/components/about-core-problems.tsx b/app/components/about-core-problems.tsx deleted file mode 100644 index 273dd5e..0000000 --- a/app/components/about-core-problems.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; -import React from "react"; -import Image from "next/image"; -import coreProblemImg from "@/public/images/problem.png"; -import coreSolutionImg from "@/public/images/solution.png"; -import Arrow from "@/public/icons/Arrow"; -import blur3 from "@/public/topBlur.svg"; // Import the blur effect SVG - -interface SectionTitleProps { - children: React.ReactNode; -} - -interface SectionTextProps { - children: React.ReactNode; -} - -interface ListItemProps { - children: React.ReactNode; -} - -interface SubListProps { - items: string[]; -} - -interface SolutionItem { - title: string; - description: string; - subItems: string[]; -} - -export const SectionTitle: React.FC = ({ children }) => ( -

- {children} -

-); - -const SectionText: React.FC = ({ children }) => ( -

- {children} -

-); - -const ListItem: React.FC = ({ children }) => ( -
  • - - {children} -
  • -); - -const SubList: React.FC = ({ items }) => ( -
      - {items.map((item, index) => ( -
    • {item}
    • - ))} -
    -); - -const AboutCoreProblems: React.FC = () => { - // Data for the component - const problems: string[] = [ - "Security vulnerabilities in centralized bridges leading to hacks and exploits", - "Inefficient capital allocation due to wrapped tokens and fragmented liquidity", - "Limited cross-chain utility of liquid staking tokens", - "High gas fees and delays in asset transfers" - ]; - - const solutionItems: SolutionItem[] = [ - { - title: "Zero-Knowledge Proofs", - description: "Instead of moving assets between chains, we use ZK-STARK proofs to verify asset ownership on Ethereum while unlocking liquidity on Starknet.", - subItems: [] - }, - { - title: "Trustless Architecture", - description: "", - subItems: [ - "Users deposit collateral on Ethereum L1.", - "ZK-STARK proof verifies the deposit without exposing sensitive data.", - "Starknet validates the proof and enables borrowing from liquidity vaults.", - "Users can immediately access trading, lending, and borrowing capabilities." - ] - }, - { - title: "Capital Efficiency", - description: "", - subItems: [ - "No wrapped tokens required.", - "Assets remain secure on Ethereum while being utilized on Starknet.", - "Liquid staking tokens can be used cross-chain without unstaking.", - "Instant settlement through proof verification." - ] - } - ]; - - return ( -
    -
    - Glow Effect -
    - - {/* Problems Section */} -
    -
    - Core Problems - - It is not a new thing that Traditional cross-chain solutions face - critical challenges such as: - -
      - {problems.map((problem, index) => ( - {problem} - ))} -
    -
    -
    -
    - Problem illustration -
    -
    -
    - - {/* Solutions Section */} -
    -
    -
    - Solution illustration -
    -
    -
    - The Solution - - There is no doubt that ZeroXBridge is solving these problems through - these innovative approaches: - -
      - {solutionItems.map((solution, index) => ( -
      - - {solution.title} - {solution.description ? `: ${solution.description}` : ""} - - {solution.subItems.length > 0 && } -
      - ))} -
    -
    -
    -
    - ); -}; - -export default AboutCoreProblems; \ No newline at end of file diff --git a/app/components/about-header.tsx b/app/components/about-header.tsx deleted file mode 100644 index 1c6d9dc..0000000 --- a/app/components/about-header.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import Image from "next/image"; -import { Button } from "./ui/button"; -import Link from "next/link"; -import { SectionTitle } from "./about-core-problems"; - -const aboutUs = { - title: 'About Us', - description: 'ZeroXBridge is more than a cross-chain solutions, it is a Revolution', - activities: [ - { - title: "OUR MISSION", - description: "We plan to revolutionize cross-chain liquidity by enabling trustless, secure asset settlement between Ethereum and Starknet without the risks of traditional bridge transfers.", - }, - { - title: "OUR VISION", - description: "We aspire to be a DeFi ecosystem where users can seamlessly access their Ethereum assets liquidity on Starknet while keeping their assets securely locked on L1.", - }, - ], -}; - -const AboutHeader = () => { - return ( -
    -
    - {aboutUs.title} -
    - -

    - {aboutUs.description} -

    - -
    -
    -
    - { - aboutUs.activities.map((item) => { - return ( -
    -

    {item.title}

    -

    {item.description}

    -
    - ) - }) - } -
    - - - -
    -
    - Glow Effect -
    -
    -
    -
    - Glow Effect -
    -
    -
    - ); -}; - -export default AboutHeader; diff --git a/app/components/about-team.tsx b/app/components/about-team.tsx deleted file mode 100644 index 6b256a9..0000000 --- a/app/components/about-team.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; -import React from "react"; -import { Manrope, Roboto_Serif } from "next/font/google"; -import Image from "next/image"; -import blocky from "@/public/blocky.svg"; -import ugo from "@/public/ugo-x.svg"; -import x from "@/public/XLogo.svg"; -import github from "@/public/GithubLogo.svg"; -import blur1 from "@/public/aboutTeamBlur.svg"; -import blur2 from "@/public/aboutTeamBlur2.svg"; -import blur3 from "@/public/topBlur.svg"; - -const manrope = Manrope({ - subsets: ["latin"], - weight: ["400", "600", "700"], -}); -const roboto = Roboto_Serif({ - subsets: ["latin"], - weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], -}); - -const AboutTeam = () => { - return ( -
    -
    - Glow Effect -
    -
    -
    - Glow Effect -
    -
    - Glow Effect -
    - -

    - Meet the Team -

    -

    - ZeroXBridge's Team leads -

    -
    -
    - ugo-x -

    Ugo-X

    -

    Blockchain Developer

    -

    Co-Founder

    - -
    -
    - blocky -

    BlockJ

    -

    Blockchain Developer

    -

    Co-Founder

    - -
    -
    -
    -
    - ); -}; - -export default AboutTeam; diff --git a/app/components/about-technology.tsx b/app/components/about-technology.tsx deleted file mode 100644 index ae2ee27..0000000 --- a/app/components/about-technology.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import Image from "next/image"; -import pointer from "../../public/up-pointer.png" -import pointerdown from "../../public/down-pointer.png" -import pointerdownDesktop from "../../public/down-pointer-desktop.png" -import blur3 from "@/public/outerBlur.svg"; -import { useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; - -interface AccordionItemProps { - title: string; - description: string; - content: string[]; - isOpen: boolean; - onClick: () => void; -} - -const AccordionItem = ({ title, description, content, isOpen, onClick }: AccordionItemProps) => { - return ( -
    -
    -
    - - - {isOpen && ( - -
    - {description} -
      - { - content.map((item, index) => ( -
    • - {item} -
    • - )) - } -
    -
    -
    - )} -
    -
    - ); -}; - -const AboutTech = () => { - const [openIndex, setOpenIndex] = useState(0); - - const accordionItems = [ - { - title: "How ZeroXBridge differs", - description: "ZeroXBridge continues to stand out from others, and here are some ways:", - content: [ - "Traditional bridges fragment liquidity across multiple chains, reducing efficiency. ZeroXBridge maintains liquidity cohesion by enabling cross-chain transactions daily without worry of efficiency problems.", - "Centralized bridges introduce single point of failure and custodial risks. ZeroXBridge uses decentralized, trustless zk-STARK proof to ensure security and transparency.", - "Traditional bridges require moving assets between chains, thereby exposing them to security risks like hacks and exploits. ZeroXBridge eliminates this by keeping your collateral securely locked on Ethereum while unlocking liquidity on Starknet." - ] - }, - { - title: "What is ZK Proofs", - description: "", - content: [] - }, - { - title: "Key differences between ZeroXBridge and Traditional Bridges", - description: "", - content: [] - }, - { - title: "Security Benefits", - description: "", - content: [] - } - ]; - - return ( -
    -
    - Glow Effect -
    -
    -
    -
    -

    - Our Technology -

    -

    Terms and their explanations

    -
    - - {/* Mobile Accordion */} -
    - {accordionItems.map((item, index) => ( - setOpenIndex(openIndex === index ? -1 : index)} - /> - ))} -
    - - {/* Desktop Two Columns */} -
    - {/* Left Column - Titles */} -
    - {accordionItems.map((item, index) => ( - - ))} -
    - - {/* Right Column - Content */} -
    - {accordionItems.map((item, index) => ( -
    -

    - {item.description} -

    -
      - { - item.content.map((content, index) => ( -
    • - {content} -
    • - )) - } -
    -
    - ))} -
    -
    -
    -
    -
    - ); -}; - -export default AboutTech; \ No newline at end of file diff --git a/app/components/about.tsx b/app/components/about.tsx deleted file mode 100644 index aebb532..0000000 --- a/app/components/about.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import React from "react"; -import Image from "next/image"; -import { Manrope, Roboto_Serif } from "next/font/google"; -import JoinCommunity from "./join-community"; - -const manrope = Manrope({ - weight: ["700"], - subsets: ["latin"], -}); - -const robotoSerif = Roboto_Serif({ - weight: ["300"], - subsets: ["latin"], -}); - -const AboutUs = () => { - return ( -
    - {/* Eclipse Background */} -
    - Background Glow -
    - - {/* Main Section Content */} -
    - {/* Title */} -

    - ZeroXBridge is here to
    - Redefine Cross-Chain Liquidity -

    - -

    - With ZeroXBridge eliminating the vulnerabilities of traditional cross-chain solutions: -

    - -
    - {/* No Asset Transfers */} -
    -
    -
    - No Asset Transfers -
    -
    -

    - No Asset Transfers -

    -

    - Traditional bridges require moving assets between chains, exposing them - to security risks like hacks and exploits. ZeroXBridge eliminates this - by keeping your collateral securely locked on Ethereum while unlocking - liquidity on Starknet. -

    -
    -
    - - - {/* No Centralized Intermediaries */} -
    -
    - No Centralized Intermediaries -
    - -
    -

    - No Centralized Intermediaries -

    -

    - Centralized bridges introduce single points of failure and custodial risks. - ZeroXBridge uses decentralized, trustless zk-STARK proofs to ensure security - and transparency. -

    -
    -
    - -
    - - {/* No Liquidity Fragmentation */} -
    -
    - Liquidity Chain - Liquidity Chart -
    - -

    - No Liquidity Fragmentation -

    -

    - Traditional bridges fragment liquidity across multiple chains, - reducing efficiency. ZeroXBridge maintains liquidity cohesion by - enabling cross-chain transactions without moving assets. You can - make numerous transactions daily without worry of efficiency problems. -

    -
    - -
    -
    - - -
    - ); -}; - -export default AboutUs; diff --git a/app/components/analyticgraph.tsx b/app/components/analyticgraph.tsx deleted file mode 100644 index e4678f0..0000000 --- a/app/components/analyticgraph.tsx +++ /dev/null @@ -1,211 +0,0 @@ -"use client"; -import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; -import { useState, useEffect } from "react"; -import { FilterIcon } from "lucide-react"; - -interface DataItem { - month: string; - thisYear: number; - lastYear: number; -} - -const data: DataItem[] = [ - { month: "JAN", thisYear: 1000, lastYear: 800 }, - { month: "FEB", thisYear: 2000, lastYear: 1600 }, - { month: "MAR", thisYear: 1800, lastYear: 1800 }, - { month: "APR", thisYear: 2200, lastYear: 2000 }, - { month: "MAY", thisYear: 2400, lastYear: 2200 }, - { month: "JUN", thisYear: 3200, lastYear: 2400 }, - { month: "JUL", thisYear: 3800, lastYear: 2800 }, - { month: "AUG", thisYear: 4000, lastYear: 3000 }, - { month: "SEP", thisYear: 3800, lastYear: 3200 }, - { month: "OCT", thisYear: 3600, lastYear: 3400 }, - { month: "NOV", thisYear: 3800, lastYear: 3600 }, - { month: "DEC", thisYear: 4000, lastYear: 3800 }, -]; - -const timeRanges = ["ALL", "6M", "3M", "1M", "1W"]; - -export default function Analytictable( {isDarkMode}: {isDarkMode: boolean} ) { - const [selectedRange, setSelectedRange] = useState("ALL"); - const [filteredData, setFilteredData] = useState(data); - const [isFilterOpen, setIsFilterOpen] = useState(false); - const [isMounted, setIsMounted] = useState(false); - - // Add effect to handle client-side rendering - useEffect(() => { - setIsMounted(true); - }, []); - - const handleFilterChange = (selectedMonth: string) => { - if (selectedMonth === "ALL") { - setFilteredData(data); - } else { - const filtered = data.filter(item => item.month === selectedMonth); - setFilteredData(filtered); - } - setIsFilterOpen(false); - }; - - return ( -
    -
    -
    -
    $0.00
    -
    -100.00% (-29.51) this week
    -
    -
    - - {isFilterOpen && ( -
    - - {data.map((item) => ( - - ))} -
    - )} -
    -
    -
    -
    - -
    -
    -
    -
    - {["Total Users", "Total Projects", "Operating Status"].map((label) => ( - - ))} -
    - -
    -
    -
    - This year -
    -
    -
    - Last year -
    -
    -
    - - {/* Add explicit height to this flex container */} -
    -
    - {timeRanges.map((range) => ( - - ))} -
    - - {/* Only render chart when mounted & with explicit height */} -
    - {isMounted && ( - - - - - - - - - - - - - - - { - if (active && payload && payload.length) { - return ( -
    -
    -
    - This Year - {payload[0].value} -
    -
    - Last Year - {payload[1].value} -
    -
    -
    - ); - } - return null; - }} - /> - - -
    -
    - )} -
    -
    -
    -
    -
    -
    -
    - ); -} \ No newline at end of file diff --git a/app/components/analytics.tsx b/app/components/analytics.tsx deleted file mode 100644 index 66efa1b..0000000 --- a/app/components/analytics.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import { cn } from "@/lib/utils"; -import { useState, useEffect } from "react"; -import { - LineChart, - Line, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer, - PieChart, - Pie, - Cell, - CartesianGrid, -} from "recharts"; - -const DUMMY_DATA = [ - { hour: "10am", value: 85 }, - { hour: "11am", value: 45 }, - { hour: "12am", value: 90 }, - { hour: "01am", value: 55 }, - { hour: "02am", value: 30 }, - { hour: "03am", value: 85 }, - { hour: "04am", value: 15 }, - { hour: "05am", value: 95 }, - { hour: "06am", value: 90 }, - { hour: "07am", value: 120 }, -]; - -const LOCKED_ASSETS_COLORS = ["#5088FF", "#D456FD"]; -const XZB_BALANCE_COLORS = ["#9E8FFF", "#FF9000"]; - -interface AnalyticsDashboardProps { - isDarkMode: boolean; - isConnected?: boolean; -} - -export default function AnalyticsDashboard({ - isDarkMode, - isConnected = false, -}: AnalyticsDashboardProps) { - const [mounted, setMounted] = useState(false); - const [activeMetric, setActiveMetric] = useState("TVL"); - - useEffect(() => { - setMounted(true); - }, []); - - const lockedAssets = [ - { name: "ETH", value: 8000, amount: "10.5 ETH", display: "$21,000" }, - { name: "USDC", value: 5000, amount: "5000 USDC", display: "$5,000" }, - ]; - - const xzbBalance = [ - { name: "xZB 1", value: 12520, amount: "26,000 xZB", display: "$26,520" }, - { name: "xZB 2", value: 10200, amount: "10,000 xZB", display: "$10,200" }, - ]; - - if (!mounted) { - return null; - } - - const cardBg = isDarkMode ? "#21192F" : "#F8F4FF"; - const textPrimary = isDarkMode ? "text-white" : "text-[#09050E]"; - const textSecondary = isDarkMode ? "text-gray-400" : "text-[#53436D]"; - const borderColor = isDarkMode ? "border-[#614199]" : "border-[#F8F4FF]"; - const tabActiveBg = isDarkMode ? "#7D53C4" : "#ECE1FF"; - const chartGridColor = isDarkMode ? "#6B7280" : "#D1D5DB"; - const tooltipBg = isDarkMode ? "#282433" : "#FBF9FF"; - const emptyChartColor = isDarkMode ? "#8B8B8B" : "#53436D"; - - const MetricCard = ({ - title, - value, - change = null, - }: { - title: string; - value: string; - change?: number | null; - }) => ( -
    -

    {title}

    -

    ${value}

    - {change !== null && ( -

    0 ? "text-[#4AD991]" : "text-red-500" - }`} - > - {change > 0 ? "+" : ""} - {change}% (24h) -

    - )} -
    - ); - - const CustomDonut = ({ - data, - colors, - isEmpty = false, - }: { - data: { name: string; value: number }[]; - colors: string[]; - isEmpty?: boolean; - }) => ( - - - {data.map((entry, index) => ( - - ))} - - - ); - - return ( -
    -
    - - - -
    - -
    -
    -
    -
    -

    - Protocol Metrics -

    -
    - {["TVL", "Volume", "Price"].map((metric) => ( - - ))} -
    -
    - - - - - - - - - - - - - - - - -
    -
    - - {/* Right Column - Donut Charts */} -
    - {/* Locked Assets */} -
    -

    - Your Locked Assets -

    -
    - -
    - {isConnected ? ( - lockedAssets.map((asset, index) => ( -
    -
    -
    -

    - {asset.amount} -

    -

    - {asset.display} -

    -
    -
    - )) - ) : ( - <> -
    -
    -
    -

    0 xZB

    -

    $0.00

    -
    -
    -
    -
    -
    -

    0 ETH

    -

    $0.00

    -
    -
    - - )} -
    -
    -
    - - {/* xZB Balance */} -
    -

    - Your xZB Balance -

    -
    - -
    - {isConnected ? ( - xzbBalance.map((balance, index) => ( -
    -
    -
    -

    - {balance.amount} -

    -

    - {balance.display} -

    -
    -
    - )) - ) : ( - <> -
    -
    -
    -

    0 xZB

    -

    $0.00

    -
    -
    -
    -
    -
    -

    0 xZB

    -

    $0.00

    -
    -
    - - )} -
    -
    -
    -
    -
    -
    - ); -} diff --git a/app/components/claim-burn.tsx b/app/components/claim-burn.tsx deleted file mode 100644 index 50ecd1a..0000000 --- a/app/components/claim-burn.tsx +++ /dev/null @@ -1,608 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React, { useState, useEffect } from "react"; -import { ChevronDown, Info } from "lucide-react"; -import Image from "next/image"; -import { useEthereum } from "./Ethereum-provider"; -import { ethers } from "ethers"; - -interface TokenData { - available: number; - balance: number; - usdValue: number; -} - -interface Asset { - id: string; - name: string; - symbol: string; - icon: string; - address?: string; -} - -interface XZBInterfaceProps { - tokenData: TokenData; - onClaim: (asset: string) => void; - onBurn: (amount: string, asset: string) => void; - isConnected: boolean | undefined; - isDarkMode: boolean; - isLoading?: boolean; -} - -const SAMPLE_ASSETS: Asset[] = [ - { - id: "1", - name: "Ethereum", - symbol: "ETH", - icon: "/token.svg", - address: "0x0000000000000000000000000000000000000000" // ETH address - }, - { - id: "2", - name: "USD Coin", - symbol: "USDC", - icon: "/token.svg", - address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC address - }, - { - id: "3", - name: "Tether", - symbol: "USDT", - icon: "/token.svg", - address: "0xdAC17F958D2ee523a2206206994597C13D831ec7" // USDT address - }, -]; - -const XZBInterface: React.FC = ({ - tokenData, - onClaim, - onBurn, - isConnected, - isDarkMode, - isLoading, -}) => { - const [activeTab, setActiveTab] = useState<"claim" | "burn">("claim"); - const [selectedAsset, setSelectedAsset] = useState(null); - const [burnAmount, setBurnAmount] = useState(""); - const [isAssetSelectorOpen, setIsAssetSelectorOpen] = useState(false); - const [hasBurned, setHasBurned] = useState(false); - const [error, setError] = useState(null); - const [isProcessing, setIsProcessing] = useState(false); - const [userEthBalance, setUserEthBalance] = useState(null); - const [maxAmount, setMaxAmount] = useState(""); - - const { - isConnected: isEthereumConnected, - // connect: connectEthereum, - // depositAsset, - // claimTokens, - address: ethereumAddress, - provider - } = useEthereum(); - - // Reset error when tab changes - useEffect(() => { - setError(null); - }, [activeTab]); - - // Add effect to fetch ETH balance when connected - useEffect(() => { - const fetchBalance = async () => { - if (isEthereumConnected && ethereumAddress && provider) { - try { - const balance = await provider.getBalance(ethereumAddress); - const balanceInEth = ethers.formatEther(balance); - setUserEthBalance(balanceInEth); - // Set max amount slightly less than balance to account for gas - const maxForGas = Number(balanceInEth) * 0.95; // Leave 5% for gas - setMaxAmount(maxForGas.toString()); - } catch (error) { - console.error("Error fetching balance:", error); - setUserEthBalance(null); - } - } else { - setUserEthBalance(null); - setMaxAmount(""); - } - }; - - fetchBalance(); - }, [isEthereumConnected, ethereumAddress, provider]); - - const calculateRedemptionAmount = (amount: string) => { - const value = parseFloat(amount) || 0; - return (value * 0.97).toFixed(2); // 3% fee - }; - - const handleAssetSelect = (asset: Asset) => { - setSelectedAsset(asset); - setIsAssetSelectorOpen(false); - setError(null); - }; - - // Update input validation - const handleAmountChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (value === "" || /^\d*\.?\d*$/.test(value)) { // Allow empty or decimal numbers - setBurnAmount(value); - - // Clear error if amount is valid - if (selectedAsset?.symbol === "ETH" && userEthBalance) { - const inputAmount = Number(value); - const maxAllowed = Number(maxAmount); - - if (inputAmount > maxAllowed) { - setError(`Amount exceeds available balance (${Number(userEthBalance).toFixed(5)} ETH). Please enter a smaller amount.`); - } else { - setError(null); - } - } - } - }; - - // Update MAX button handler - const handleMaxClick = () => { - if (selectedAsset?.symbol === "ETH" && maxAmount) { - setBurnAmount(maxAmount); - setError(null); - } else { - setBurnAmount(tokenData.balance.toString()); - } - }; - - const handleBurn = async (amount: string, assetId: string) => { - try { - setError(null); - setIsProcessing(true); - - // Check if wallet is connected - if (!isConnected) { - throw new Error("Please connect your wallet first"); - } - - // Check if Ethereum wallet is connected for L1 - if (!isEthereumConnected || !ethereumAddress) { - try { - // await connectEthereum(); - await new Promise(resolve => setTimeout(resolve, 1000)); - - if (!isEthereumConnected || !ethereumAddress) { - throw new Error("Failed to connect Ethereum wallet"); - } - } catch (error: any) { - console.error('error', error) - throw new Error("Please connect your Ethereum wallet first"); - } - } - - if (!selectedAsset) { - throw new Error("Please select an asset first"); - } - - if (!amount || parseFloat(amount) <= 0) { - throw new Error("Please enter a valid amount"); - } - - // Determine asset type (0 for ETH, 1 for ERC20) - const assetType = selectedAsset.symbol === "ETH" ? 0 : 1; - - // Get asset address - const tokenAddress = selectedAsset.address || "0x0000000000000000000000000000000000000000"; - - // For ETH transfers, validate balance before proceeding - if (assetType === 0) { - const balance = await provider?.getBalance(ethereumAddress); - if (!balance) { - throw new Error("Could not get your ETH balance"); - } - - const amountInWei = ethers.parseEther(amount); - if (balance < amountInWei) { - const balanceInEth = ethers.formatEther(balance); - throw new Error( - `Insufficient ETH balance. You have ${Number(balanceInEth).toFixed(5)} ETH but trying to send ${amount} ETH. Please reduce the amount or add more ETH to your wallet.` - ); - } - } - - console.log("Initiating burn transaction:", { - assetType, - tokenAddress, - amount, - userAddress: ethereumAddress, - isEthereumConnected, - hasAddress: !!ethereumAddress - }); - - // Call depositAsset function - // const txHash = await depositAsset( - // assetType, - // tokenAddress, - // amount - // ); - - // console.log("Transaction hash:", txHash); - setHasBurned(true); - onBurn(amount, assetId); - } catch (error) { - console.error("Error in handleBurn:", error); - const errorMessage = error instanceof Error ? error.message : "An error occurred while burning tokens"; - setError(errorMessage); - - // If it's an insufficient funds error, add a suggestion - if (errorMessage.includes("Insufficient")) { - setTimeout(() => { - setError(errorMessage + "\n\nTip: Try reducing the amount or adding more funds to your wallet."); - }, 100); - } - } finally { - setIsProcessing(false); - } - }; - - const handleClaim = async () => { - try { - setError(null); - setIsProcessing(true); - - if (!isEthereumConnected || !ethereumAddress) { - // await connectEthereum(); - return; - } - - // await claimTokens(); - onClaim(selectedAsset?.id || ""); - } catch (error) { - console.error("Error in handleClaim:", error); - setError(error instanceof Error ? error.message : "An error occurred while claiming tokens"); - } finally { - setIsProcessing(false); - } - }; - - const handleConnect = async () => { - try { - setError(null); - setIsProcessing(true); - - if (activeTab === "claim" && !isEthereumConnected) { - // await connectEthereum(); - // Wait a bit for the connection to be established - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Check if connection was successful - if (!isEthereumConnected || !ethereumAddress) { - throw new Error("Failed to connect Ethereum wallet"); - } - } - } catch (error) { - console.error("Error connecting:", error); - setError(error instanceof Error ? error.message : "An error occurred while connecting"); - } finally { - setIsProcessing(false); - } - }; - - return ( -
    - {/* Tab Buttons */} -
    - - -
    - -
    - {error && ( -
    -

    {error}

    -
    - )} - - {/* Title */} -

    - {activeTab === "claim" - ? "Claim Your xZB Tokens Now" - : "Burn Your xZB Tokens Now"} -

    - - {/* Burn First Warning */} - {activeTab === "claim" && !hasBurned && ( -
    -

    - Please burn your tokens first before claiming. This ensures proper token redemption. -

    -
    - )} - - {/* Balance Display */} -
    -

    - {activeTab === "claim" ? "Available to claim" : "xZB Balance"} -

    -

    - {activeTab === "claim" - ? tokenData.available.toLocaleString() - : tokenData.balance.toLocaleString()}{" "} - xZB -

    -

    - -${tokenData.usdValue.toLocaleString()} -

    -
    - - {activeTab === "burn" && ( -
    -

    - Enter amount to Burn - {selectedAsset?.symbol === "ETH" && userEthBalance && ( - - (Available: {Number(userEthBalance).toFixed(5)} ETH) - - )} -

    -
    - - -
    -
    - )} - - {/* Asset Selector */} -
    -

    - {activeTab === "burn" ? "Receive Asset" : "Select Asset"} -

    - - - {/* Dropdown Menu */} - {isAssetSelectorOpen && ( -
    - {SAMPLE_ASSETS.map((asset) => ( - - ))} -
    - )} -
    - - {/* Info Box */} -
    - {activeTab === "claim" ? ( -
    -

    - You will receive xZB Tokens on Starknet after claiming -

    -
    - ) : ( - <> -
    -
    - - Redemption Fee - - 3% -
    -
    - - You will receive - - {calculateRedemptionAmount(burnAmount)} ETH -
    -
    -
    - Burning xZB is irreversible and subject to a 3% redemption fee -
    - - )} - {/* Learn More */} - -
    - - {/* Action Button */} - {isConnected ? ( - - ) : ( - - )} -
    -
    - ); -}; - -export default XZBInterface; diff --git a/app/components/connectWallet.tsx b/app/components/connectWallet.tsx deleted file mode 100644 index 3da903e..0000000 --- a/app/components/connectWallet.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import React, { useState } from 'react'; -import { useAccount as useStarknetAccount } from '@starknet-react/core'; -import { useAccount as useEthereumAccount } from 'wagmi'; -import { X } from 'lucide-react'; -import StarknetWalletModal from './starknetWallet'; -import EthereumWalletModal from './ethereumWallet'; - -interface ConnectModalProps { - isModalOpen: boolean; - setIsModalOpen: (isModalOpen: boolean) => void; -} - -const ConnectModal: React.FC = ({ isModalOpen, setIsModalOpen }) => { - const [activeChain, setActiveChain] = useState<'ethereum' | 'starknet' | null>(null); - - const { address: starknetAddress } = useStarknetAccount(); - const { address: ethereumAddress } = useEthereumAccount(); - - const handleOverlayClick = () => { - setIsModalOpen(false); - }; - - const handleModalClick = (e: React.MouseEvent) => { - e.stopPropagation(); - }; - - const handleBack = () => { - setActiveChain(null); - }; - - return ( -
    -
    - {/* Close Button */} - - - {/* Title */} -

    - {activeChain === null - ? 'Select a Network' - : activeChain === 'ethereum' - ? 'Connect Ethereum Wallet' - : 'Connect Starknet Wallet' - } -

    - - {/* Content */} -
    - {activeChain === null ? ( -
    - {/* Ethereum Option */} - - - {/* Starknet Option */} - -
    - ) : activeChain === 'ethereum' ? ( - - ) : ( - - )} -
    -
    -
    - ); -}; - -export default ConnectModal; \ No newline at end of file diff --git a/app/components/dapp/governance/learn-overview.tsx b/app/components/dapp/governance/learn-overview.tsx new file mode 100644 index 0000000..b9b014d --- /dev/null +++ b/app/components/dapp/governance/learn-overview.tsx @@ -0,0 +1,101 @@ +// import { ArrowUpRight, BookOpen } from "@/public/icons/svg/general"; +import { ArrowRight, BookOpen } from "lucide-react"; +import Link from "next/link"; +const resources = [ + { + title: "Starknet’s progressive governance", + description: + "A decentralized network that strives to evolve over time needs to have progressively evolving decentralized governance mechanisms to support protocol upgrades.", + link: "#", + }, + { + title: "How to delegate voting power", + description: + "If you are a STRK token holder, you can select a delegate to vote in your place for protocol changes.", + link: "#", + }, +]; +export const LearnOverview = () => { + return ( +
    +
    +
    +

    + +

    + Learn +

    +

    +

    + Starknet Governance overview +

    +
    +
    +
    +

    + Overview +

    +

    + Starknet is a permissionless decentralized Layer 2 (L2) validity + rollup, built to enable Ethereum to scale by using cryptographic + protocols called STARKs, without compromising Ethereum’s core + principles of decentralization, transparency, inclusivity, and + security. +

    +
    + +
    +
    +
    +
    + {resources.map((item, index) => ( + + ))} +
    +
    + ); +}; +interface OverviewCardProps { + title: string; + description: string; + link: string; +} +export const OverviewCard = ({ + title, + description, + link, +}: OverviewCardProps) => { + return ( + +
    +
    +

    {title}

    +

    + {description} +

    +
    +
    +

    + Learn more +

    + + + +
    +
    + + ); +}; diff --git a/app/components/deposit.tsx b/app/components/deposit.tsx deleted file mode 100644 index 990ab40..0000000 --- a/app/components/deposit.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import React, { useState } from "react"; -import { X } from "lucide-react"; -import { -// useContract, - useAccount, - useBalance, -// useSendTransaction -} from "@starknet-react/core"; -import { useTheme } from "../ThemeContext"; -// import { uint256 } from "starknet"; - -interface DepositProps { - token: string; - onClose: () => void; -} - -const Deposit: React.FC = ({ token, onClose }) => { - const [amount, setAmount] = useState(""); - const [error, setError] = useState(""); - const { isDarkMode } = useTheme(); - - // Starknet React Hooks - const { account } = useAccount(); -// // Get contract instance -// const { contract } = useContract({ -// address: `0x${contractAddress}`, -// abi: [], // Add your contract ABI here -// }); - - // Get token balance - const { data: balance, isLoading: isLoadingBalance } = useBalance({ - address: `0x${account?.address}`, - token: `0x${token}`, - }); - -// // Add transaction to transaction manager -// const { send } = useSendTransaction({ -// calls: [ -// { -// contractAddress: contractAddress, -// entrypoint: "deposit", -// calldata: [], -// }, -// ], -// }); - -// const handleDeposit = async () => { -// if (!contract || !account) { -// setError("Please connect your wallet"); -// return; -// } - -// try { -// setError(""); - -// // Convert amount to uint256 -// const amountInWei = uint256.bnToUint256( -// BigInt(parseFloat(amount) * 10 ** 18) -// ); - -// // Update calldata with the correct values -// const tx = send([ -// { -// contractAddress: contractAddress, -// entrypoint: "deposit", -// calldata: [ -// account.address, -// amountInWei.low, -// amountInWei.high, -// ], -// }, -// ]); -// console.log('transaction result', tx); -// // Clear input and close modal -// setAmount(""); -// onClose(); - -// } catch (err) { -// setError(err instanceof Error ? err.message : "Failed to deposit"); -// } -// }; - - // Format balance for display - const formattedBalance = balance - ? parseFloat(balance.toString()) / 10 ** 18 - : "0"; - - return ( -
    - {/* Header with close button */} -
    -

    Deposit {token}

    - -
    - -
    -
    - - -
    - -
    -
    - - - Balance: {isLoadingBalance ? "Loading..." : formattedBalance} - -
    - { - // Only allow numbers and one decimal point - const value = e.target.value.replace(/[^0-9.]/g, ""); - if (value.split(".").length <= 2) { - setAmount(value); - } - }} - /> - -
    -
    - - {error && ( -

    {error}

    - )} - - -
    - ); -}; - -export default Deposit; \ No newline at end of file diff --git a/app/components/ethereumWallet.tsx b/app/components/ethereumWallet.tsx deleted file mode 100644 index e554702..0000000 --- a/app/components/ethereumWallet.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import { useConnect } from 'wagmi'; -import { ChevronLeft } from 'lucide-react'; - -interface EthereumWalletModalProps { - onBack: () => void; -} - -const EthereumWalletModal: React.FC = ({ onBack }) => { - const { connect, connectors } = useConnect(); - - const walletIcons: Record = { - metamask: '/icons/wallets/metamask.svg', - coinbase: '/icons/wallets/coinbase-logo.svg', - walletconnect: '/icons/wallets/walletconnect.svg', - injected: '/wallet.svg', - }; - - return ( -
    - {/* Back Button */} - - - {/* Wallet List */} -
    - {connectors.map((connector: any) => ( - - ))} -
    - - {/* Help Text */} -

    - New to Ethereum?{' '} - - Learn more about wallets - -

    -
    - ); -}; - -export default EthereumWalletModal; \ No newline at end of file diff --git a/app/components/footer.tsx b/app/components/footer.tsx deleted file mode 100644 index eeef758..0000000 --- a/app/components/footer.tsx +++ /dev/null @@ -1,150 +0,0 @@ -"use client" - -import Image from 'next/image' -import Link from 'next/link' -import { Manrope } from 'next/font/google' - -const manrope = Manrope({ - subsets: ["latin"], - weight: ["400", "500", "600", "700"], -}) - -const Footer = () => { - const footerLinks = { - product: [ - { name: 'About Us', href: '/about' }, - { name: 'FAQs', href: '#' }, - { name: 'Documentation', href: '#' }, - { name: 'Prices', href: '#' }, - ], - company: [ - { name: 'Career', href: '#' }, - { name: 'Contact US', href: '#' }, - { name: 'Address', href: '#' }, - { name: 'Developers', href: '#' }, - ], - socials: [ - { name: 'Telegram', href: '#' }, - { name: 'Twitter', href: '#' }, - { name: 'Discord', href: '#' }, - { name: 'Github', href: '#' }, - ], - } - - return ( -
    - -
    -
    - ZEROXBRIDGE -
    -
    - -
    -
    - {/* Logo and Description Section */} -
    -
    -
    - ZeroXBridge Logo -
    -
    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Nunc vulputate libero et velit interdum, ac aliquet odio - mattis. Class aptent taciti sociosqu ad litora torquent per - conubia nostra, per inceptos. -

    -
    - - {/* Links Section */} -
    -
    -

    - PRODUCT -

    -
      - {footerLinks.product.map((link) => ( -
    • - - {link.name} - -
    • - ))} -
    -
    - -
    -

    - COMPANY -

    -
      - {footerLinks.company.map((link) => ( -
    • - - {link.name} - -
    • - ))} -
    -
    - -
    -

    - SOCIALS -

    -
      - {footerLinks.socials.map((link) => ( -
    • - - {link.name} - -
    • - ))} -
    -
    -
    -
    -
    -
    - ) -} - -export default Footer \ No newline at end of file diff --git a/app/components/header.tsx b/app/components/header.tsx deleted file mode 100644 index e3a5898..0000000 --- a/app/components/header.tsx +++ /dev/null @@ -1,286 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -"use client"; - -import { useState, useEffect } from "react"; -import Image from "next/image"; -import RightArrow from "@/public/right-arrow.svg"; -import Link from "next/link"; -import { Button } from "./ui/button"; - -interface StatItem { - value: string; - label: string; - endValue: number; -} - -const STATS_DATA: StatItem[] = [ - { value: "70M+", label: "Total Transactions", endValue: 70 }, - { value: "7K+", label: "Active Users", endValue: 7 }, - { value: "20M+", label: "Total earned", endValue: 20 }, - { value: "10M+", label: "Investments", endValue: 10 }, -]; - -const NETWORK_NODES = [ - { top: "5.7%", left: "26%", translateX: "0", translateY: "0", delay: "0s" }, - { top: "6%", left: "67.7%", translateX: "0", translateY: "0", delay: "1.5s" }, - { - top: "38.5%", - left: "83.5%", - translateX: "0", - translateY: "-50%", - delay: "0.7s", - }, - { - top: "71.5%", - left: "90.9%", - translateX: "0", - translateY: "0", - delay: "2.2s", - }, - { - top: "48%", - left: "51%", - translateX: "-50%", - translateY: "0", - delay: "1.2s", - }, - { - top: "85%", - left: "19%", - translateX: "0", - translateY: "0", - delay: "2.8s", - }, - { - top: "58%", - left: "4%", - translateX: "0", - translateY: "-50%", - delay: "0.4s", - }, - { - top: "47%", - left: "9%", - translateX: "0", - translateY: "0", - delay: "3.3s", - }, - { - top: "32%", - left: "5%", - translateX: "0", - translateY: "0", - delay: "1.8s", - }, - { top: "67%", left: "46%", translateX: "0", translateY: "0", delay: "2.5s" }, - { - top: "37.5%", - left: "55.4%", - translateX: "0", - translateY: "0", - delay: "0.9s", - }, - { top: "52%", left: "77%", translateX: "0", translateY: "0", delay: "0.9s" }, - { top: "54%", left: "90%", translateX: "0", translateY: "0", delay: "3.1s" }, - { - top: "92.7%", - left: "70%", - translateX: "0", - translateY: "0", - delay: "3.1s", - }, - { top: "93%", left: "42%", translateX: "0", translateY: "0", delay: "3.1s" }, - { - top: "87%", - left: "30.5%", - translateX: "0", - translateY: "0", - delay: "3.1s", - }, - { - top: "78.4%", - left: "59.5%", - translateX: "0", - translateY: "0", - delay: "3.1s", - }, - { - top: "66.5%", - left: "28%", - translateX: "0", - translateY: "0", - delay: "3.1s", - }, -]; - -const Header = () => { - const [is4K, setIs4K] = useState(false); - const [counts, setCounts] = useState(STATS_DATA.map(() => 0)); - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - // Check if screen is 4K or higher - const check4K = () => { - setIs4K(window.innerWidth >= 3000); - }; - - check4K(); - window.addEventListener("resize", check4K); - return () => window.removeEventListener("resize", check4K); - }, []); - - // Then in your node mapping: - // Add a small adjustment to positions for 4K screens - const getNodePosition = (node: any) => { - if (is4K) { - // Adjust position for 4K screens - fine-tune these values - return { - top: `calc(${node.top} + 0.4%)`, - left: `calc(${node.left} - 2.2%)`, - }; - } - return { top: node.top, left: node.left }; - }; - - useEffect(() => { - setIsVisible(true); - }, []); - - useEffect(() => { - if (!isVisible) return; - - const animationDuration = 2000; - const steps = 60; - const interval = animationDuration / steps; - - const animations = STATS_DATA.map((stat, index) => { - let currentStep = 0; - return setInterval(() => { - if (currentStep === steps) { - clearInterval(animations[index]); - return; - } - - setCounts((prevCounts) => { - const newCounts = [...prevCounts]; - newCounts[index] = Math.min( - Math.ceil((stat.endValue * currentStep) / steps), - stat.endValue - ); - return newCounts; - }); - - currentStep++; - }, interval); - }); - - return () => animations.forEach((interval) => clearInterval(interval)); - }, [isVisible]); - - const renderStatItem = (stat: StatItem, index: number) => ( -
    - Right Arrow -
    -
    - {counts[index]} - {stat.value.slice(-2)} -
    -
    - {stat.label} -
    -
    -
    - ); - - return ( -
    -
    -
    -
    -

    - Secure Cross-Chain Liquidity with Zero-Knowledge Proofs -

    -
    -

    - Unlock liquidity on Starknet using Ethereum collateral—no asset - transfers, -

    -

    - no wrapping, no centralized bridges. -

    -
    -
    - - - -
    -
    - -
    - {/* Spinning globe (inner element) */} -
    - Spinning Globe -
    - - {/* Static network overlay (outer element) */} -
    - Network Overlay -
    - - {/* Flashing nodes */} -
    - {NETWORK_NODES.map((node, index) => { - const position = getNodePosition(node); - return ( -
    -
    -
    - ); - })} -
    -
    -
    -
    -
    - {STATS_DATA.map(renderStatItem)} -
    -
    -
    -
    - ); -}; - -export default Header; diff --git a/app/components/how-it-works.tsx b/app/components/how-it-works.tsx deleted file mode 100644 index ff52f82..0000000 --- a/app/components/how-it-works.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React, { useState, useEffect } from 'react'; -import Image from 'next/image'; -import wallet from '../../public/wallet.svg'; -import write from '../../public/write.svg'; -import scroll from '../../public/scroll.svg'; -import token from '../../public/token.svg'; -import blur3 from "@/public/outerBlur.svg"; - -const steps = [ - { - number: 1, - title: "Connect Wallet", - image: wallet, - description: "First, you'll have to connect your wallet and then deposit collateral (ETH, USDC, STRK etc) on Ethereum L1." - }, - { - number: 2, - title: "ZK-STARK Proof", - image: write, - description: "A ZK-STARK proof verifies the deposit without exposing sensitive data." - }, - { - number: 3, - title: "STARKNET VERIFIES PROOF", - image: scroll, - description: "Starknet verifies the proof and unlocks borrowing power from liquidity vaults." - }, - { - number: 4, - title: "BORROW, LEND OR TRADE", - image: token, - description: "And that's all, you can now borrow, lend, or trade using their collateralized funds." - } -]; - -const HowItWorks = () => { - const [currentStep, setCurrentStep] = useState(0); - const [isAnimating, setIsAnimating] = useState(false); - - useEffect(() => { - const timer = setInterval(() => { - setIsAnimating(true); - setTimeout(() => { - setCurrentStep((prev) => (prev + 1) % steps.length); - setTimeout(() => { - setIsAnimating(false); - }, 500); // Allow transition to complete before removing animation class - }, 500); // Time to fade out current step - }, 5000); - - return () => clearInterval(timer); - }, []); - - const handleStepChange = (index: any) => { - if (currentStep === index || isAnimating) return; // Prevent changing during animation - setIsAnimating(true); - setTimeout(() => { - setCurrentStep(index); - setTimeout(() => { - setIsAnimating(false); - }, 500); - }, 500); - }; - - return ( -
    - {/* Background Effects */} -
    - Glow Effect -
    - - {/* For Large Screens */} - Glow Effect - -
    - {/* Section Title */} -
    -

    - How it Works -

    -

    - These easy 4 steps are all you need to Get Started -

    -
    - - {/* Content Grid */} -
    - {/* Image Section */} -
    -
    - {steps.map((step, index) => ( -
    -
    -
    - {step.title} -
    -
    -
    - ))} -
    -
    - - {/* Text Section */} -
    -
    -
    -
    - {steps[currentStep].number}. -
    -
    -

    - {steps[currentStep].title} -

    -

    - {steps[currentStep].description} -

    - - {/* Navigation Dots */} -
    - {steps.map((_, index) => ( -
    -
    -
    -
    -
    -
    -
    - - -
    - ); -}; - -export default HowItWorks; \ No newline at end of file diff --git a/app/components/join-community.tsx b/app/components/join-community.tsx deleted file mode 100644 index 2d433ea..0000000 --- a/app/components/join-community.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Manrope, Roboto_Serif } from "next/font/google"; -import Image from "next/image"; -import ellipse from "@/public/join-community/ellipse.svg"; - -const manrope = Manrope({ - weight: ["700"], - subsets: ["latin"], -}); - -const robotoSerif = Roboto_Serif({ - weight: ["400"], - subsets: ["latin"], -}); - -const JoinCommunity = () => { - return ( -
    -
    - community ellipse with blur filter -
    -
    -

    - Join Our Wonderful Community Today! -

    -

    - Stay Connected with exciting updates, do not be left out of this - revolution -

    -
    -
    - -
    -
    -
    -
    - ); -}; - - -export default JoinCommunity; \ No newline at end of file diff --git a/app/components/lock-liquidity.tsx b/app/components/lock-liquidity.tsx deleted file mode 100644 index 769934b..0000000 --- a/app/components/lock-liquidity.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client"; -import { useAccount } from "@starknet-react/core"; -import Image from "next/image"; -import { useState } from "react"; -import Deposit from "./deposit"; - -interface LiquidityLockTableProps { - isDarkMode: boolean; - className?: string; -} - -const LiquidityLockTable = ({ - isDarkMode, - className, -}: LiquidityLockTableProps) => { - const { isConnected } = useAccount() - const [depositToken, setDepositToken] = useState('SOL'); - const [DepositModal, setDepositModal] = useState(false); - const liquidityRows = [ - { - token: "SOL", - currentPrice: "$177.08", - currentLiquidity: "$500,000", - xzTokenRate: "0.01", - }, - { - token: "SOL", - currentPrice: "$177.08", - currentLiquidity: "$500,000", - xzTokenRate: "0.01", - }, - { - token: "SOL", - currentPrice: "$177.08", - currentLiquidity: "$500,000", - xzTokenRate: "0.01", - }, - { - token: "SOL", - currentPrice: "$177.08", - currentLiquidity: "$500,000", - xzTokenRate: "0.01", - }, - { - token: "SOL", - currentPrice: "$177.08", - currentLiquidity: "$500,000", - xzTokenRate: "0.01", - }, - { - token: "SOL", - currentPrice: "$177.08", - currentLiquidity: "$500,000", - xzTokenRate: "0.01", - }, - ]; - - const showDepositModal = (token: string) => { - setDepositToken(token); - setDepositModal(true); - }; - - const handleClose = () => { - setDepositModal(false); - } - - return ( -
    -

    - Lock Liquidity by making a deposit -

    - {/* This div handles the overflow internally */} -
    - - - - - - - - - - - {liquidityRows.map((row, index) => ( - - - - - - - ))} - - -
    - Token - Current LiquidityxZB Token Rate - Lock Amount -
    - Token icon - {row.token} - - {row.currentLiquidity} -
    {row.currentPrice}
    -
    {row.xzTokenRate} - -
    - - {DepositModal && ( - - )} -
    -
    - ); -}; - -export default LiquidityLockTable; diff --git a/app/components/lock-summary.tsx b/app/components/lock-summary.tsx deleted file mode 100644 index 8041494..0000000 --- a/app/components/lock-summary.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client" - -import { Info, X } from "lucide-react"; - -export default function LockSummary() { - - const handleInputChange = (event: React.FormEvent) => { - const target = event.currentTarget; - target.value = target.value.replace(/[^0-9.]/g, ""); - }; - - return ( -
    -
    - -

    - Lock Summary -

    - - - -
    -
    -

    Token

    -

    USDC

    -
    - -
    -

    - xZB to Receive -

    -

    0 xZB

    -
    - -
    -

    Token

    -

    Low Risk

    -
    - -
    -

    - Assets remain securely Locked on Ethereum while you use xZB on - Starknet -

    -
    - -
    - -

    Requires signatures from both Ethereum and Starknet wallets

    -
    -
    - - -
    -
    - ); -} diff --git a/app/components/mobile-navigator.tsx b/app/components/mobile-navigator.tsx deleted file mode 100644 index b73ef66..0000000 --- a/app/components/mobile-navigator.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { Landmark, LayoutGrid } from "lucide-react"; -import { PiChartPieSlice, PiCoins, PiSwap } from "react-icons/pi"; -import { useTheme } from "../ThemeContext"; - -const NavigationBar = () => { - const router = usePathname(); - const {isDarkMode} = useTheme() - const currentPath = router; - - const navItems = [ - { name: "Dashboard", path: "/dashboard", icon: LayoutGrid }, - { name: "Swap", path: "/dashboard/swap", icon: PiSwap }, - { name: "Claim", path: "/dashboard/claim-burn", icon: Landmark }, - { name: "Lock", path: "/dashboard/lock-liquidity", icon: PiCoins }, - { name: "Analytics", path: "/dashboard/analytics", icon: PiChartPieSlice }, - ]; - - return ( -
    -
    - {navItems.map((item) => { - const isActive = currentPath === item.path; - const activeColor = "#A26DFF"; - const inactiveColor = isDarkMode ? "#D4D4D4" : "#53436D"; - - const IconComponent = item.icon; - - return ( - -
    - {isActive && ( -
    - )} -
    - -
    - - {item.name} - -
    - - ); - })} -
    -
    - ); -}; - -export default NavigationBar; \ No newline at end of file diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx deleted file mode 100644 index 506b637..0000000 --- a/app/components/navbar.tsx +++ /dev/null @@ -1,420 +0,0 @@ -"use client"; -import { - Search, - Moon, - Sun, - Menu, - X, - SearchIcon, - Bell, - Settings, -} from "lucide-react"; -import Notification from "../../public/bell.png"; -import Image from "next/image"; -import Logo from "../../public/zerologo.png"; -import LogoWhite from "../../public/zerologo-white.svg"; -import Link from "next/link"; -import { useState } from "react"; -import { useEffect } from "react"; -import ConnectModal from "./connectWallet"; -import { useAccount } from "@starknet-react/core"; -import { useWalletState } from "../hooks/useWalletState"; -// import { useRegistration } from '../hooks/useRegistration'; - -interface NavbarProps { - isDarkMode: boolean; - toggleDarkMode: () => void; - onConnectWallet?: () => void; -} - -const Navbar: React.FC = ({ isDarkMode, toggleDarkMode }) => { - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - const [disconnectModal, setdisconnectModalOpen] = useState(false); - const { isConnected } = useAccount(); - - const toggleMobileMenu = () => { - setMobileMenuOpen(!mobileMenuOpen); - }; - - const { - // isAnyWalletConnected, - isAllConnected, - getDisplayAddress, - ethereumAddress, - starknetAddress, - disconnectEthereum, - disconnectStarknet, - disconnectAll, - } = useWalletState(); - - // const { registerUser, isRegistering } = useRegistration(); - - // useEffect(() => { - // const handleRegistration = async () => { - // if (isAllConnected) { - // try { - // await registerUser(); - // // You could add a success notification here - // } catch (err) { - // console.error("Registration failed:", err); - // // You could add an error notification here - // } - // } - // }; - - // handleRegistration(); - // }, [isAllConnected, registerUser]); - - const handleConnectWallet = () => { - if (isAllConnected) { - // disconnectAll(); - setdisconnectModalOpen(true); - } else { - setIsModalOpen(true); - } - }; - - useEffect(() => { - if (isConnected) { - setIsModalOpen(false); - } - }, [isConnected]); - - const gradientBorder = - "bg-gradient-to-b from-[#A26DFF] to-[#A26DFF] p-[0.7px] rounded-full"; - - return ( - <> -
    - {/* Logo section with border */} - - - - - {/* Main content */} -
    -
    - {/* Search Input with gradient border */} -
    -
    -
    - -
    -
    - -
    - - {/* Notification with gradient border */} -
    -
    - bell -
    -
    -
    - - {/* Mobile menu button */} -
    - -
    - - {/* Right side controls */} -
    -
    - {/* Dark mode toggle with gradient border */} - {isDarkMode ? ( - - ) : ( - - )} -
    - - - {/* Connect Wallet button with gradient border */} -
    - -
    -
    -
    -
    - - {isModalOpen && ( -
    - {/* Backdrop */} -
    setIsModalOpen(false)} - /> - {/* Modal Content */} -
    - -
    -
    - )} - - {disconnectModal && ( -
    - {/* Backdrop */} -
    setdisconnectModalOpen(false)} - /> - {/* Modal Content */} -
    -
    - {/* Ethereum Option */} - - - {/* Starknet Option */} - - - {/* Disconnect all */} - -
    -
    -
    - )} - - {/* Mobile Menu Overlay */} -
    -
    - {/* Mobile Menu Header */} -
    - {/* Logo for mobile */} - - ZeroxBridge Logo - - - {/* Close button */} - -
    - - {/* Menu Content */} -
    - {/* Navigation Links */} - -
    -
    -
    - - ); -}; - -export default Navbar; diff --git a/app/components/rippleSVG.tsx b/app/components/rippleSVG.tsx deleted file mode 100644 index 3d89b0c..0000000 --- a/app/components/rippleSVG.tsx +++ /dev/null @@ -1,435 +0,0 @@ -"use client" - -import type React from "react" -import { useEffect, useRef } from "react" -import { motion, useAnimation } from "framer-motion" - -const GlowingSvg: React.FC = () => { - const controls1 = useAnimation() - const controls2 = useAnimation() - const controls3 = useAnimation() - const controls4 = useAnimation() - const isMounted = useRef(false) - - useEffect(() => { - // Set the component as mounted - isMounted.current = true - - // Set initial opacity states - controls1.set({ opacity: 0.2 }) - controls2.set({ opacity: 0.2 }) - controls3.set({ opacity: 0.4 }) - controls4.set({ opacity: 0.4 }) - - const startRippleSequence = async () => { - // Check if still mounted before starting each animation sequence - if (!isMounted.current) return - - try { - // Start with innermost circle (controls4) completely disappearing - await controls4.start({ - opacity: [0.8, 0.2, 0], - transition: { duration: 1, ease: "easeInOut" } - }) - - if (!isMounted.current) return - - // Second ring now pulses dramatically (appearing and fading) - await controls3.start({ - opacity: [0.6, 1, 0.2], - transition: { duration: 1.5, ease: "easeInOut" } - }) - - if (!isMounted.current) return - - // Third ring pulses next - await controls2.start({ - opacity: [0.6, 1, 0.2], - transition: { duration: 1.5, ease: "easeInOut" } - }) - - if (!isMounted.current) return - - // Outermost ring completes the ripple effect - await controls1.start({ - opacity: [0.4, 1, 0.2], - transition: { duration: 1.5, ease: "easeInOut" } - }) - - if (!isMounted.current) return - - // Reset the innermost ring to prepare for next sequence - await controls4.start({ - opacity: 0.4, - transition: { duration: 0.5, ease: "easeOut" } - }) - } catch (error) { - // Handle potential errors if component unmounts during animation - console.error("Animation error:", error) - } - } - - // Wait a bit longer to ensure DOM is fully ready - const initialTimeout = setTimeout(() => { - if (isMounted.current) { - startRippleSequence() - } - }, 500) // Increased from 100ms to 500ms - - // Then repeat it at intervals - const interval = setInterval(() => { - if (isMounted.current) { - startRippleSequence() - } - }, 6500) - - return () => { - isMounted.current = false - clearTimeout(initialTimeout) - clearInterval(interval) - } - }, []) // Removed the dependencies that might cause unnecessary re-renders - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} - -export default GlowingSvg \ No newline at end of file diff --git a/app/components/starknetWallet.tsx b/app/components/starknetWallet.tsx deleted file mode 100644 index 7f7e218..0000000 --- a/app/components/starknetWallet.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -"use client"; - -import Image from "next/image"; -import { useState } from "react"; -import { useConnect } from "@starknet-react/core"; -import { X } from "lucide-react"; - -interface ConnectModalProps { - onBack: () => void; -} - -export default function ConnectModal({ - onBack -}: ConnectModalProps) { - // StarkNet React hooks - const { connect, connectors } = useConnect(); - - const handleModalClick = (e: React.MouseEvent) => { - e.stopPropagation(); // Prevent closing modal when clicking inside - }; - - - - return ( -
    - - {/* Modal Container */} -
    - {/* Close Button */} - - - {/* Title */} -

    - Select a wallet -

    - - {/* Subtitle */} -

    - Securely authenticate & start earning. -

    - - {/* Wallet List */} -
    - {connectors.map((wallet, idx) => ( -
    { - connect({ connector: wallet }); - onBack(); - }} - > - - - {/* Divider between wallet items, except after last one */} - {idx < connectors.length - 1 && ( -
    - )} -
    - ))} -
    -
    -
    - ); -} \ No newline at end of file diff --git a/app/components/success-state/animated-icon.tsx b/app/components/success-state/animated-icon.tsx deleted file mode 100644 index 68e20cb..0000000 --- a/app/components/success-state/animated-icon.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Image from "next/image"; -import wallet1 from "../../../public/success-state/wallet1.svg"; -import wallet2 from "../../../public/success-state/wallet2.svg"; -import burn1 from "../../../public/success-state/burn1.svg"; -import burn2 from "../../../public/success-state/burn2.svg"; -import swap1 from "../../../public/success-state/swap1.svg"; -import swap2 from "../../../public/success-state/swap2.svg"; -import lock1 from "../../../public/success-state/lock1.svg"; -import lock2 from "../../../public/success-state/lock2.svg"; - - - -type IconType = "claim" | "swap" | "burn" | "lock"; // Add more types as needed - -interface AnimatedIconProps { - iconType: IconType; // Type of icon set // Time between transitions (ms) -} - -const iconSets: Record = { - claim: [wallet1, wallet2], - burn: [burn1, burn2], - swap: [swap1, swap2], - lock: [lock1, lock2], -}; - -const AnimatedIcon: React.FC = ({ iconType }) => { - const icons = iconSets[iconType]; - const [currentIconIndex, setCurrentIconIndex] = useState(0); - - useEffect(() => { - const iconInterval = setInterval(() => { - setCurrentIconIndex(1); - }, 700); - - return () => clearInterval(iconInterval); // Cleanup on unmount - }, [icons]); - - return ( -
    - {`${iconType} -
    - ); -}; - -export default AnimatedIcon; \ No newline at end of file diff --git a/app/components/success-state/success-state.tsx b/app/components/success-state/success-state.tsx deleted file mode 100644 index 1be8bec..0000000 --- a/app/components/success-state/success-state.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { Manrope} from "next/font/google"; -import { X } from 'lucide-react'; -import Link from 'next/link'; - -const manrope = Manrope({ - weight: ["400", "700"], - subsets: ['latin'], - preload: true -}); - -interface SuccessStateProps { - isOpen: boolean; - isDarkMode: boolean; - onClose: React.Dispatch>; - message: string; - icon: React.ReactNode; -} - -const SuccessState: React.FC = ({ - isOpen, - isDarkMode, - onClose, - message, - icon, -}) => { - if (!isOpen) return null; - - return ( -
    -
    -
    - - -
    - {icon} -
    -

    - CONGRATULATIONS! -

    - -

    - {message} -

    - - - Go to Dashboard - -
    -
    - ); -}; - -export default SuccessState; \ No newline at end of file diff --git a/app/components/swap.tsx b/app/components/swap.tsx deleted file mode 100644 index c6d7cdd..0000000 --- a/app/components/swap.tsx +++ /dev/null @@ -1,564 +0,0 @@ -"use client"; -import { useState } from "react"; -import { - ChevronDown, - Settings, - ArrowUpDown, - Wallet2, - AlarmClock, - FuelIcon, -} from "lucide-react"; -import Image from "next/image"; -import { useTheme } from "../ThemeContext"; -import { useAccount } from "@starknet-react/core"; - -const tokens = ["ETH", "SOL"]; - -const Swap = () => { - const [fromValue, setFromValue] = useState("0.00034"); - const [toValue, setToValue] = useState("5.79"); - const [fromToken, setFromToken] = useState("ETH"); - const [toToken, setToToken] = useState("SOL"); - const [showFromDropdown, setShowFromDropdown] = useState(false); - const [showToDropdown, setShowToDropdown] = useState(false); - const { isDarkMode } = useTheme(); - const {isConnected} = useAccount(); - - const handleSwap = () => { - setFromToken(toToken); - setToToken(fromToken); - setFromValue(toValue); - setToValue(fromValue); - }; - - return ( -
    - {isConnected ? ( -
    -
    -
    -

    - Swap Tokens -

    - -
    - -
    -
    -
    - setFromValue(e.target.value)} - className={`bg-transparent text-xl font-semibold w-full focus:outline-none ${ - isDarkMode ? "text-white" : "text-[#1F1333]" - }`} - /> -

    - - $3.85 USD -

    -
    - -

    - 7.04{" "} - - MAX - -

    -
    -
    - -
    - - - {showFromDropdown && ( -
    - {tokens.map((token) => ( -
    { - setFromToken(token); - setShowFromDropdown(false); - }} - > - {token} -
    - ))} -
    - )} -
    -
    -
    - -
    - -
    - -
    -
    -
    - setToValue(e.target.value)} - className={`${isDarkMode ? "text-white" : "text-[#1F1333]"} - bg-transparent text-xl font-semibold w-full focus:outline-none`} - /> -

    - - $3.9 USD (-1.24%) -

    - -
    - -

    - 0.00{" "} -

    -
    -
    - -
    - - - {showToDropdown && ( -
    - {tokens.map((token) => ( -
    { - setToToken(token); - setShowToDropdown(false); - }} - > - {token} -
    - ))} -
    - )} -
    -
    - -
    - -
    -
    -

    Price:

    -0.7785 USDT per Eth

    -
    -
    -

    Frontend Fee:

    $0

    -
    -
    - -
    -
    - {toToken} - - $70 - -
    - -
    - - -

    - ~$0.01 -

    -
    -
    - -
    -

    Advanced Option

    - -
    - - -
    - - -
    - ) : ( -
    -
    -
    -
    -

    - From -

    -

    - $10 -

    -
    - -
    - - - {showFromDropdown && ( -
    - {tokens.map((token) => ( -
    { - setFromToken(token); - setShowFromDropdown(false); - }} - > - {token} -
    - ))} -
    - )} -
    -
    -
    - -
    - -
    - -
    -
    -
    -

    To

    -

    - $70 -

    -
    - -
    - - - {showToDropdown && ( -
    - {tokens.map((token) => ( -
    { - setToToken(token); - setShowToDropdown(false); - }} - > - {token} -
    - ))} -
    - )} -
    -
    -
    - - -
    - )} -
    - ); -}; - -export default Swap; diff --git a/app/components/testimonial.tsx b/app/components/testimonial.tsx index c23398a..fa3242c 100644 --- a/app/components/testimonial.tsx +++ b/app/components/testimonial.tsx @@ -1,64 +1,65 @@ -"use client" +"use client"; -import { useState, useEffect } from "react" -import Image from "next/image" -import GlowingProtractorSVG from "./rippleSVG"; +import { useState, useEffect } from "react"; +import Image from "next/image"; +import GlowingProtractorSVG from "../../public/bitcoin.svg"; +import { useTranslation } from "react-i18next"; -import { motion } from 'framer-motion'; +import { motion } from "framer-motion"; interface Testimonial { - id: number - content: string + id: number; + content: string; author: { - name: string - image: string - } + name: string; + image: string; + }; } -const testimonials: Testimonial[] = [ - { - id: 1, - content: - "Traditional bridges require moving assets between chains, exposing them to security risks like hacks and exploits. ZeroXBridge eliminates this by keeping your collateral securely locked on Ethereum while unlocking liquidity on Starknet.", - author: { - name: "Elon White", - image: "/images/testimonial-card-profile.png", +// Move testimonials inside the component where t is available + +export default function Testimonial() { + const [currentSlide, setCurrentSlide] = useState(0); + const { t } = useTranslation(); + + const testimonials: Testimonial[] = [ + { + id: 1, + content: t("testimonial.content"), + author: { + name: t("testimonial.author"), + image: "/images/testimonial-card-profile.png", + }, }, - }, - { - id: 2, - content: - "Traditional bridges require moving assets between chains, exposing them to security risks like hacks and exploits. ZeroXBridge eliminates this by keeping your collateral securely locked on Ethereum while unlocking liquidity on Starknet.", - author: { - name: "Elon White", - image: "/images/testimonial-card-profile.png", + { + id: 2, + content: t("testimonial.content"), + author: { + name: t("testimonial.author"), + image: "/images/testimonial-card-profile.png", + }, }, - }, - { - id: 3, - content: - "Traditional bridges require moving assets between chains, exposing them to security risks like hacks and exploits. ZeroXBridge eliminates this by keeping your collateral securely locked on Ethereum while unlocking liquidity on Starknet.", - author: { - name: "Elon White", - image: "/images/testimonial-card-profile.png", + { + id: 3, + content: t("testimonial.content"), + author: { + name: t("testimonial.author"), + image: "/images/testimonial-card-profile.png", + }, }, - } -] - -export default function Testimonial() { - const [currentSlide, setCurrentSlide] = useState(0) + ]; useEffect(() => { const interval = setInterval(() => { - setCurrentSlide((prevSlide) => (prevSlide + 1) % testimonials.length) - }, 5000) + setCurrentSlide((prevSlide) => (prevSlide + 1) % testimonials.length); + }, 5000); - return () => clearInterval(interval) - }, []) + return () => clearInterval(interval); + }, [testimonials.length]); const handleManualNavigation = (index: number) => { - setCurrentSlide(index) - } + setCurrentSlide(index); + }; return (
    @@ -75,21 +76,22 @@ export default function Testimonial() { {/* Main Container */}
    - {/* Header */}

    - Hear what people are saying about us + {t("testimonial.title")}

    - Don't be left out of this Revolution + {t("testimonial.subtitle")}

    {/* Glowing SVG Lines */} -
    + w-full sm:w-auto flex justify-center items-center" + >
    @@ -97,34 +99,37 @@ export default function Testimonial() {
    {testimonials.map((testimonial, index) => { - const offset = (index - currentSlide + testimonials.length) % testimonials.length - let translateX = "0%" - let zIndex = 0 - let opacity = 1 - let visibilityClass = "block" // Default visible + const offset = + (index - currentSlide + testimonials.length) % + testimonials.length; + let translateX = "0%"; + let zIndex = 0; + let opacity = 1; + let visibilityClass = "block"; // Default visible if (offset === 0) { - zIndex = 3 + zIndex = 3; } else if (offset === 1 || offset === testimonials.length - 1) { - translateX = offset === 1 ? "105%" : "-105%" - zIndex = 2 - opacity = 0.7 + translateX = offset === 1 ? "105%" : "-105%"; + zIndex = 2; + opacity = 0.7; } else { - translateX = offset === 2 ? "210%" : "-210%" - zIndex = 1 - opacity = 0.4 + translateX = offset === 2 ? "210%" : "-210%"; + zIndex = 1; + opacity = 0.4; } // Hide side cards on mobile if (offset !== 0) { - visibilityClass = "hidden sm:block" + visibilityClass = "hidden sm:block"; } return ( @@ -148,18 +158,22 @@ export default function Testimonial() {
    {testimonial.author.name} - {testimonial.author.name} + + {testimonial.author.name} +
    - ) + ); })} -
    @@ -184,5 +198,5 @@ export default function Testimonial() {
    - ) -} \ No newline at end of file + ); +} diff --git a/app/components/tokenclaim.tsx b/app/components/tokenclaim.tsx deleted file mode 100644 index cf895d4..0000000 --- a/app/components/tokenclaim.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; -import { useRouter } from "next/navigation"; // Import Next.js router - -interface TokenClaimProps { - isConnected: boolean | undefined; - onConnect: () => void; - isDarkMode: boolean; -} - -export default function TokenClaim({ isConnected, onConnect, isDarkMode }: TokenClaimProps) { - const router = useRouter(); // Initialize the router - - const handleClick = () => { - if (isConnected) { - router.push("/dashboard"); //this routes to the get startedd page - } else { - onConnect(); // this connects the wallet - } - }; - - return ( -
    -
    -

    Claim/Burn XZB Tokens

    - -
    -
    - ); -} diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index 47535d2..3e7968c 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -1,67 +1,63 @@ +"use client"; + import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +// Simple cn function without external dependencies +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} -const buttonVariants = cva( - "relative inline-flex items-center rounded-full justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-[#4C327A] text-white hover:bg-opacity-90", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - gradientPrimary: [ - "relative p-[2px] text-white", - "before:absolute before:inset-0 before:rounded-full", - "before:bg-[linear-gradient(20deg,#A26DFF,#4C327A,#A26DFF,#A26DFF)]", - "before:bg-[length:400%_100%]", - "before:content-[''] before:z-[0]", - "before:animate-[rotate_6s_linear_infinite]", - "after:absolute after:inset-[2px] after:rounded-full", - "after:bg-[#4C327A] after:z-[1]", - ].join(" "), - }, - size: { - default: "h-12", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, +// Simple button variants without cva dependency +const getButtonClasses = (variant: string = "default", size: string = "default") => { + const baseClasses = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50" + + const variantClasses = { + default: "bg-[#a26dff] text-white shadow hover:bg-[#907DBD]", + destructive: "bg-red-600 text-white shadow hover:bg-red-700", + outline: "border border-[#a26dff] bg-transparent text-[#a26dff] hover:bg-[#a26dff] hover:text-white", + secondary: "bg-gray-600 text-white shadow hover:bg-gray-700", + ghost: "hover:bg-[#a26dff]/20 hover:text-[#a26dff]", + link: "text-[#a26dff] underline-offset-4 hover:underline", + } + + const sizeClasses = { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md gap-1.5 px-3", + lg: "h-10 rounded-md px-6", + icon: "size-9", } -) + + return cn( + baseClasses, + variantClasses[variant as keyof typeof variantClasses] || variantClasses.default, + sizeClasses[size as keyof typeof sizeClasses] || sizeClasses.default + ) +} -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { +interface ButtonProps extends React.ComponentProps<"button"> { + variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" + size?: "default" | "sm" | "lg" | "icon" asChild?: boolean } -const Button = React.forwardRef( - ({ className, variant, size, children, ...props }, ref) => { - if (variant === 'gradientPrimary') { - return ( - - ) - } +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: ButtonProps) { + const Comp = asChild ? "span" : "button" // Simplified, no Slot dependency - return ( - - ) : ( -
    // Placeholder to maintain layout - )} -
    - ${balance} -
    -
    - -

    You can add another Address and also switch Addresses

    -
    -
    - ) -} - diff --git a/app/config.ts b/app/config.ts index 2804055..1ddadc0 100644 --- a/app/config.ts +++ b/app/config.ts @@ -1,19 +1,14 @@ -import { http, createConfig } from 'wagmi' -import { base, mainnet} from 'wagmi/chains' -import { injected, metaMask, safe, walletConnect } from 'wagmi/connectors' - -const projectId = '' +import { createConfig, http } from 'wagmi'; +import { mainnet, sepolia } from 'wagmi/chains'; +import { injected } from '@wagmi/connectors'; export const config = createConfig({ - chains: [mainnet, base], - connectors: [ - injected(), - walletConnect({ projectId }), - metaMask(), - safe(), - ], + chains: [mainnet, sepolia], transports: { [mainnet.id]: http(), - [base.id]: http(), + [sepolia.id]: http(), }, -}) \ No newline at end of file + connectors: [ + injected(), + ], +}); diff --git a/app/context/ConnectionContext.tsx b/app/context/ConnectionContext.tsx new file mode 100644 index 0000000..d9534bf --- /dev/null +++ b/app/context/ConnectionContext.tsx @@ -0,0 +1,32 @@ +'use client' +import { createContext, useContext, useState, ReactNode } from "react"; +interface ConnectionContextType { + isConnected: boolean; + setIsConnected: (connected: boolean) => void; + walletAddress: string | null; + setWalletAddress: (address: string | null) => void; +} + +const ConnectionContext = createContext(undefined) +interface ConnectionProviderProps { + children: ReactNode; +} + +export const ConnectionProvider = ({ children }: ConnectionProviderProps) => { +const [isConnected, setIsConnected] = useState(false); + const [walletAddress, setWalletAddress] = useState(null); + + return ( + + {children} + + ); + } + + export const useConnection = (): ConnectionContextType => { + const context = useContext(ConnectionContext); + if (!context) { + throw new Error("useConnection must be used within a ConnectionProvider"); + } + return context; +}; \ No newline at end of file diff --git a/app/context/index.ts b/app/context/index.ts new file mode 100644 index 0000000..e076873 --- /dev/null +++ b/app/context/index.ts @@ -0,0 +1,2 @@ +export * from "./theme-provider" +export * from "./ConnectionContext" diff --git a/app/context/starknet-provider.tsx b/app/context/starknet-provider.tsx new file mode 100644 index 0000000..74eb094 --- /dev/null +++ b/app/context/starknet-provider.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; +import { sepolia } from "@starknet-react/chains"; +import { StarknetConfig, publicProvider } from "@starknet-react/core"; +import { starknetConnectors } from "@/lib/connectors"; + +export const StarknetProvider = ({ children }: { children: ReactNode }) => { + const provider = publicProvider(); + const chains = [sepolia]; + + return ( + + {children} + + ); +}; diff --git a/app/context/theme-provider.tsx b/app/context/theme-provider.tsx new file mode 100644 index 0000000..4fd1a58 --- /dev/null +++ b/app/context/theme-provider.tsx @@ -0,0 +1,72 @@ +"use client"; + +import React, { + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; + +export type Theme = "light" | "dark"; + +export interface ThemeContextValues { + theme: Theme; + isDark: boolean; + toggleTheme: () => void; +} + +export const ThemeContext = createContext( + undefined +); + +export const useThemeContext = () => { + const context = useContext(ThemeContext); + if (!context) + throw new Error("useThemeContext must be used within a ThemeProvider"); + return context; +}; + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [theme, setTheme] = useState("light"); + + useEffect(() => { + const storedTheme = localStorage.getItem("theme") as Theme | null; + + const prefersDark = window.matchMedia( + + "(prefers-color-scheme: dark)" + + ).matches; + const activeTheme = storedTheme || (prefersDark ? "dark" : "light"); + + setTheme(activeTheme); + updateHtmlClass(activeTheme); + }, []); + + const updateHtmlClass = (newTheme: Theme) => { + document.documentElement.classList.remove("light", "dark"); + document.documentElement.classList.add(newTheme); + }; + + const isDark = useMemo(() => theme === "dark", [theme]); + + const toggleTheme = () => { + const newTheme = theme === "dark" ? "light" : "dark"; + setTheme(newTheme); + updateHtmlClass(newTheme); + localStorage.setItem("theme", newTheme); + }; + + const contextValues: ThemeContextValues = { + theme, + isDark, + toggleTheme, + }; + + return ( + + {children} + + ); +}; diff --git a/app/contexts/LanguageContext.tsx b/app/contexts/LanguageContext.tsx new file mode 100644 index 0000000..f52fe7a --- /dev/null +++ b/app/contexts/LanguageContext.tsx @@ -0,0 +1,67 @@ +"use client"; + +import React, { createContext, useContext, useEffect, useState } from "react"; +import i18n from "../i18n-client"; + +interface LanguageContextType { + currentLanguage: string; + changeLanguage: (language: string) => Promise; + isLoading: boolean; +} + +const LanguageContext = createContext( + undefined +); + +export const useLanguage = () => { + const context = useContext(LanguageContext); + if (context === undefined) { + throw new Error("useLanguage must be used within a LanguageProvider"); + } + return context; +}; + +interface LanguageProviderProps { + children: React.ReactNode; +} + +export const LanguageProvider: React.FC = ({ + children, +}) => { + const [currentLanguage, setCurrentLanguage] = useState("en"); + const [isLoading, setIsLoading] = useState(false); + + const changeLanguage = async (language: string) => { + setIsLoading(true); + try { + await i18n.changeLanguage(language); + setCurrentLanguage(language); + if (typeof window !== "undefined") { + localStorage.setItem("language", language); + } + } catch (error) { + console.error("Error changing language:", error); + } finally { + setIsLoading(false); + } + }; + + // Initialize language from localStorage on mount + useEffect(() => { + if (typeof window !== "undefined") { + const savedLanguage = localStorage.getItem("language"); + if (savedLanguage && ["en", "fr"].includes(savedLanguage)) { + setCurrentLanguage(savedLanguage); + i18n.changeLanguage(savedLanguage); + } + } + }, []); + + return ( + + {children} + + ); +}; diff --git a/app/dapp/analytics/page.tsx b/app/dapp/analytics/page.tsx new file mode 100644 index 0000000..4f54501 --- /dev/null +++ b/app/dapp/analytics/page.tsx @@ -0,0 +1,258 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; +import ChartCard from "../components/analytics/ChartCard"; +import StatsOverview from "../components/analytics/StatsOverview"; +import AssetPieChart from "../components/analytics/AssetPieChart"; +import EmptyState from "../components/analytics/EmptyState"; +import { useThemeContext } from "@/app/hooks/context/theme"; +import { useWallet } from "@/app/hooks"; + +/** --------------------------------------------------- + * Mock data (unchanged) + * --------------------------------------------------- */ +const stats = [ + { id: "1", title: "Wallet Balance", value: "$1.13" }, + { id: "2", title: "Total Value Locked", value: "$92,294,191" }, + { id: "3", title: "24H Volume", value: "$165,003,398" }, +]; + +// Mock analytics table data +const mockTableData = [ + { asset: "ETH", price: "$1,823", change: "+3.2%" }, + { asset: "BTC", price: "$29,482", change: "-0.5%" }, + { asset: "SOL", price: "$23.45", change: "+1.8%" }, + { asset: "MATIC", price: "$0.85", change: "-2.1%" }, + { asset: "USDC", price: "$1.00", change: "0.0%" }, +]; + +/** --------------------------------------------------- + * Reusable Skeleton Block + * --------------------------------------------------- */ +const SkeletonBlock: React.FC<{ + className?: string; + theme: "light" | "dark"; +}> = ({ className = "", theme }) => ( +
    +); + +/** --------------------------------------------------- + * StatsOverview Skeleton + * --------------------------------------------------- */ +const StatsOverviewSkeleton: React.FC<{ theme: "light" | "dark" }> = ({ + theme, +}) => ( +
    + {[0, 1, 2].map((i) => ( +
    + + +
    + ))} +
    +); + +/** --------------------------------------------------- + * Chart Skeleton + * --------------------------------------------------- */ +const ChartSkeleton: React.FC<{ theme: "light" | "dark" }> = ({ theme }) => ( +
    +
    + +
    + + + +
    +
    + +
    +); + +/** --------------------------------------------------- + * Pie Chart Skeleton + * --------------------------------------------------- */ +const PieSkeleton: React.FC<{ theme: "light" | "dark" }> = ({ theme }) => ( +
    + +
    +
    + +
    +
    +
    + {[0, 1, 2, 3].map((i) => ( +
    + + +
    + ))} +
    +
    +); + +/** --------------------------------------------------- + * Table Skeleton + * --------------------------------------------------- */ +const TableSkeleton: React.FC<{ theme: "light" | "dark" }> = ({ theme }) => ( +
    + +
    + {[0, 1, 2, 3, 4].map((i) => ( +
    + + + +
    + ))} +
    +
    +); + +/** --------------------------------------------------- + * Mock Analytics Table + * --------------------------------------------------- */ +const AnalyticsTable: React.FC = () => ( +
    + + + + + + + + + + {mockTableData.map((row, i) => ( + + + + + + ))} + +
    AssetPrice24h Change
    {row.asset}{row.price} + {row.change} +
    +
    +); + +export default function AnalyticsPage() { + const { isConnected } = useWallet(); + const [selectedChart, setSelectedChart] = useState< + "tvl" | "volume" | "price" + >("price"); + const { theme } = useThemeContext(); + + const [loading, setLoading] = useState(true); + useEffect(() => { + const t = setTimeout(() => setLoading(false), 5000); + return () => clearTimeout(t); + }, []); + + // ✅ always resolve to a valid theme + const safeTheme: "light" | "dark" = useMemo(() => { + if (theme === "light" || theme === "dark") return theme; + return "light"; + }, [theme]); + + // ✅ Show skeletons FIRST + if (loading) { + return ( +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + ); + } + + // ✅ After skeletons finish, check wallet + if (!isConnected) { + return ; + } + + // ✅ Finally show analytics dashboard + return ( +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + ); +} diff --git a/app/dapp/claim-burn/components/info.tsx b/app/dapp/claim-burn/components/info.tsx new file mode 100644 index 0000000..bf27b3b --- /dev/null +++ b/app/dapp/claim-burn/components/info.tsx @@ -0,0 +1,38 @@ +import { Skeleton } from '../../components/Skeleton'; + +interface InfoRowProps { + label: string; + value: React.ReactNode; + isDark: boolean; + valueFontWeight?: string; + loading: boolean; +} + +export const InfoRow = ({ + label, + loading, + value, + isDark, + valueFontWeight, +}: InfoRowProps) => ( +
    + + {label} + + {loading ? ( + + ) : ( + + {value} + + )} +
    +); diff --git a/app/dapp/claim-burn/components/success.tsx b/app/dapp/claim-burn/components/success.tsx new file mode 100644 index 0000000..4a9b9f9 --- /dev/null +++ b/app/dapp/claim-burn/components/success.tsx @@ -0,0 +1,108 @@ +"use client"; + +import Image from "next/image"; +import { DialogBase } from "../../components/ui/Dailog"; +import { Geist_Mono, Inter } from "next/font/google"; +import { GlobeIcon } from "@/svg/GlobeIcon"; +import { GradientWrapperPrimary } from "../../components/ui/Gradients"; +import { useThemeContext } from "@/app/hooks/context"; + +const geistMono = Geist_Mono({ + subsets: ["latin"], +}); + +const inter = Inter({ + subsets: ["latin"], +}); + +export interface SuccessModalProps { + isOpen: boolean; + type: string; + onClose: () => void; + amount: string; +} + +export const SuccessModal = ({ + isOpen, + onClose, + type, + amount, +}: SuccessModalProps) => { + const { isDark } = useThemeContext(); + const date = new Date().toLocaleString("en-US", { + dateStyle: "long", + timeStyle: "medium", + }); + + return ( + +
    +
    + Check mark +
    + +

    + xZB {type === "claim" ? "Claimed" : "Burned"}! +

    +
    + + {type === "burn" && ( +

    + You’ve unlocked + {amount} ETH by burning + the allocated for locking {amount} xZB! +

    + )} + + {type === "claim" && ( +

    + You’ve claimed 3094 xZB for locking 3492.23 ETH! +

    + )} + +

    + {date} +

    + +
    + + + + + +
    +
    + ); +}; diff --git a/app/dapp/claim-burn/components/tab.tsx b/app/dapp/claim-burn/components/tab.tsx new file mode 100644 index 0000000..768c467 --- /dev/null +++ b/app/dapp/claim-burn/components/tab.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { useThemeContext } from '@/app/hooks/context'; +import React from 'react'; + +interface ClaimBurnTabProps { + activeTab: string; + setActiveTab: React.Dispatch>; +} + +export const ClaimBurnTab = ({ + activeTab, + setActiveTab, +}: ClaimBurnTabProps) => { + const { isDark } = useThemeContext(); + return ( +
    +
    +
    + + + + +
    +
    + ); +}; diff --git a/app/dapp/claim-burn/page.tsx b/app/dapp/claim-burn/page.tsx new file mode 100644 index 0000000..8b1e51e --- /dev/null +++ b/app/dapp/claim-burn/page.tsx @@ -0,0 +1,332 @@ +'use client'; + +import { useWallet } from '@/app/hooks/useWallet'; +import { useEffect, useMemo, useState } from 'react'; +import { ethers } from 'ethers'; +import bridgeAbi from '@/lib/bridgeAbi.json'; +import { useTranslation } from 'react-i18next'; +import { SuccessModal } from './components/success'; +import { ConnectWalletButton } from '../components/ui/ConnectWalletButton'; +import Image from 'next/image'; +import { ClaimBurnTab } from './components/tab'; +import { Geist_Mono, Inter } from 'next/font/google'; +import { Hamburger } from '@/svg/Hamburger'; +import { Info } from '@/svg/Info'; +import { useThemeContext } from '@/app/hooks/context'; +import { InfoRow } from './components/info'; + +const geistMono = Geist_Mono({ + subsets: ['latin'], +}); + +const inter = Inter({ + subsets: ['latin'], +}); + +type BurnClaimData = { + available: number; + value: string; + price: string; + fee: string; + displayAmount: string; +}; + +export default function ClaimBurnPage() { + const { isDark } = useThemeContext(); + const wallet = useWallet(); + const { t } = useTranslation ? useTranslation() : { t: (x: string) => x }; + const [activeTab, setActiveTab] = useState('claim'); + const [amount, setAmount] = useState(''); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [loading, setLoading] = useState(true); + const [registering, setRegistering] = useState(false); + const [registerStatus, setRegisterStatus] = useState(null); + + const isConnected = wallet?.isConnected; + const CLAIM_BURN_DATA: Record = useMemo( + () => ({ + claim: { + available: isConnected ? 3939 : 0, + value: isConnected ? '$3394.13' : '--', + price: isConnected ? '$0.123' : '--', + fee: '$0', + displayAmount: isConnected ? '3094.00' : '0.00', + }, + burn: { + available: isConnected ? 3939 : 0, + value: isConnected ? '$3394.13' : '--', + price: isConnected ? '$0.123' : '--', + fee: '$0', + displayAmount: isConnected ? '3094.00' : '0.00', + }, + }), + [isConnected] + ); + + // Register Starknet Key logic + const handleRegisterStarknetKey = async () => { + setRegisterStatus(null); + setRegistering(true); + try { + let ethAddress = wallet.ethAddress; + let strkAddress = wallet.strkAddress; + if (!ethAddress || !strkAddress) throw new Error("Connect both wallets first."); + ethAddress = ethers.getAddress(ethAddress); + let starknetPubKey = BigInt(strkAddress); + const provider = new ethers.BrowserProvider(window.ethereum); + const signer = await provider.getSigner(); + const encoded = ethers.solidityPacked([ + "string", + "address", + "uint256" + ], [ + "UserRegistration", + ethAddress, + starknetPubKey + ]); + const messageHash = ethers.keccak256(encoded); + + let rawSignature; + try { + rawSignature = await window.ethereum.request({ + method: 'eth_sign', + params: [ethAddress, messageHash], + }); + } catch (err) { + setRegisterStatus( + 'Registration is not possible with your current wallet (e.g., MetaMask) until the contract is updated to support standard Ethereum signatures. Please contact support or try again later.' + ); + setRegistering(false); + return; + } + + const contract = new ethers.Contract("0x8F25bFe32269632dfd8D223D51FF145414d8107b", bridgeAbi, signer); + const tx = await contract.registerUser(rawSignature, starknetPubKey); + await tx.wait(); + setRegisterStatus("Registration successful!"); + } catch (err: any) { + setRegisterStatus(err.message || "Registration failed"); + } finally { + setRegistering(false); + } + }; + + const currentData = CLAIM_BURN_DATA[activeTab]; + + const handleMaxClick = () => { + if (isConnected) setAmount(currentData.displayAmount); + }; + + const handleAction = () => { + if (isConnected && amount) setShowSuccessModal(true); + }; + + const isActionable = !!(amount && amount !== '0' && amount !== '0.00'); + const claimBurnBtnClasses = isActionable + ? isDark + ? 'bg-white text-[#515151] hover:opacity-80' + : 'bg-black text-white hover:opacity-80' + : isDark + ? 'bg-[#2e2e2e] text-[#515151] cursor-not-allowed' + : 'bg-[#f0f0f0] text-[#c4c4c4] cursor-not-allowed'; + + useEffect(() => { + const timer = setTimeout(() => setLoading(false), 2000); + return () => clearTimeout(timer); + }, []); + + return ( + <> +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + ZeroXBridge Logo +
    +
    +

    + {activeTab === 'claim' ? 'Claim' : 'Burn'} +

    +

    xZB

    +
    +
    +
    +
    + setAmount(e.target.value)} + disabled={!isConnected} + className={`no-spinner w-full py-4 text-[32px] font-light bg-transparent outline-none border-none pr-20 ${ + isDark + ? 'text-white placeholder-[#515151]' + : 'text-[var(--claim-area)] placeholder-[var(--claim-input-placeholder)]' + } ${geistMono.className}`} + /> + +
    + + +
    + +
    +
    + + + + + {activeTab === 'burn' && isConnected && amount && ( + + )} +
    + + {activeTab === 'burn' && ( +
    + +

    + Burning xZB tokens releases your locked USDC/USDT/ETH + tokens from the contract. +

    +
    + )} + + {!isConnected ? ( + + ) : ( + <> + + + {registerStatus && ( +
    + {registerStatus} +
    + )} + + )} +
    +
    +
    +
    +
    + setShowSuccessModal(false)} + type={activeTab} + amount={amount} + /> + + ); +} diff --git a/app/dapp/coming-soon/page.tsx b/app/dapp/coming-soon/page.tsx new file mode 100644 index 0000000..b51a7ed --- /dev/null +++ b/app/dapp/coming-soon/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import Image from "next/image"; +import { useTranslation } from "react-i18next"; +import "../../i18n-client"; + +const ComingSoon = () => { + const { t } = useTranslation(); + + const data = [ + { + title: t("comingSoon.seamlessWallet.title"), + description: t("comingSoon.seamlessWallet.description"), + img: ( + img + ), + }, + { + title: t("comingSoon.governanceDAO.title"), + description: t("comingSoon.governanceDAO.description"), + img: ( + img + ), + }, + { + title: t("comingSoon.accountAbstraction.title"), + description: t("comingSoon.accountAbstraction.description"), + img: ( + img + ), + }, + { + title: t("comingSoon.stakingAPY.title"), + description: t("comingSoon.stakingAPY.description"), + img: ( + img + ), + }, + { + title: t("comingSoon.paymasterIntegration.title"), + description: t("comingSoon.paymasterIntegration.description"), + img: ( + img + ), + }, + ]; + + return ( +
    +
    + {data.map((item) => ( +
    +
    + {item.title} +
    +
    + {item.description} +
    +
    + {item.img} +
    +
    + ))} +
    +
    + ); +}; + +export default ComingSoon; diff --git a/app/dapp/components/Footer.tsx b/app/dapp/components/Footer.tsx new file mode 100644 index 0000000..16df402 --- /dev/null +++ b/app/dapp/components/Footer.tsx @@ -0,0 +1,565 @@ +"use client"; + +import { easeInOut, motion } from "framer-motion"; +import { useState } from "react"; +import Image from "next/image"; +import { ArrowUpRight } from "lucide-react"; + +const Footer = () => { + const [email, setEmail] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [error, setError] = useState(""); + + const handleNewsletterSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + // Basic email regex validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setError("Please enter a valid email address."); + return; + } + + setIsSubmitting(true); + + try { + await new Promise((resolve) => setTimeout(resolve, 1500)); + + setIsSubmitted(true); + setEmail(""); + setTimeout(() => setIsSubmitted(false), 3000); + } catch { + setError("Something went wrong. Please try again."); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
    + {/* OnlyDust Promotional Banner */} +
    +
    + + {/* Content container with dark background */} +
    +
    + {/* Text content */} +
    + + We are building open-source, join at{" "} + + ONLYDUST{" "} + + + + + + + + + + + + + + + OnlyDust / ZeroXBridge + + + + + +
    + + {/* Logo on the right */} + +
    + OnlyDust Logo +
    +
    +
    +
    +
    +
    +
    + + {/* Newsletter Section */} +
    +
    + +

    + Stay in the loop! +

    +

    + Subscribe to our newsletter for the latest updates, stories, and + product announcements +

    +
    + +
    +
    + setEmail(e.target.value)} + placeholder="Enter e-mail address" + className="flex-1 px-4 py-3 bg-[#1A1A1A] w-full md:w-[400px] h-[48px] 2xl:w-[992px] 2xl:h-[78px] rounded-[12px] 2xl:rounded-[19.53px] text-white placeholder-[#97A1A4] focus:outline-none transition-colors" + required + /> + + {isSubmitting ? ( + + + + + + Subscribing... + + ) : isSubmitted ? ( + + + + + Subscribed! + + ) : ( + <> + Join Newsletter + + + + + )} + +
    + +
    + + {error &&

    {error}

    } +
    +
    + + {/* Community Section */} +
    +
    +
    + +

    + Join our +
    + + + + + + + Community! + + + + + + +

    +

    + ZeroXBridge will enable the community +
    + to vote on which assets get whitelisted +
    + and help shape key protocol decisions. +

    +
    + + + {/* Discord */} + +
    + + + + + + + + + + +
    + + + +
    +
    +
    +

    Discord

    + +
    +
    + + {/* Telegram */} + +
    + + + + + + + + + +
    + + + + + +
    +
    +
    +

    Telegram

    + +
    +
    +
    +
    +
    +
    + + {/* Footer Links and Copyright */} +
    +
    +

    + Quick Links +

    + +
    +
    + + + + + + + + + + + +
    + +
    +
    +
    +

    + All Rights Reserved, © ZeroXBridge, 2025 +

    + + + Use the App + + + + +
    +
    +
    +
    + ); +}; + +export default Footer; diff --git a/app/dapp/components/Navbar.tsx b/app/dapp/components/Navbar.tsx new file mode 100644 index 0000000..b19da9f --- /dev/null +++ b/app/dapp/components/Navbar.tsx @@ -0,0 +1,117 @@ +"use client"; +import ArrowIcon from './ui/ArrowIcon'; +import { Menu, X } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useState } from 'react'; + +const Navbar = () => { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + return ( +
    +
    + + logo + +
    + + + + {/* Mobile menu icon */} + + + {/* Mobile sidebar navigation */} +
    +
    +
    + setMobileMenuOpen(false)}> + logo + +
    + +
    + +
    + + {/* Overlay when sidebar is open */} + {mobileMenuOpen && ( +
    setMobileMenuOpen(false)} + /> + )} +
    + ); +}; + +export default Navbar; diff --git a/app/dapp/components/PageHeader.tsx b/app/dapp/components/PageHeader.tsx new file mode 100644 index 0000000..a2f4ccd --- /dev/null +++ b/app/dapp/components/PageHeader.tsx @@ -0,0 +1,43 @@ +import Navbar from "./Navbar"; + +interface PageHeaderProps { + title: string; + description?: string; + showNavbar?: boolean; +} + +const PageHeader = ({ + title, + description, + showNavbar = true +}: PageHeaderProps) => { + return ( +
    + {showNavbar && } +
    +
    +

    + {title} +

    + {description && ( +

    + {description} +

    + )} +
    +
    +
    +
    + ); +}; + +export default PageHeader; \ No newline at end of file diff --git a/app/dapp/components/Skeleton.tsx b/app/dapp/components/Skeleton.tsx new file mode 100644 index 0000000..176769a --- /dev/null +++ b/app/dapp/components/Skeleton.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import clsx from 'clsx'; + +interface SkeletonProps extends React.HTMLAttributes { + className?: string; +} + +export const Skeleton: React.FC = ({ className, ...props }) => { + return ( +
    + ); +}; diff --git a/app/components/Starknet-provider.tsx b/app/dapp/components/Starknet-provider.tsx similarity index 76% rename from app/components/Starknet-provider.tsx rename to app/dapp/components/Starknet-provider.tsx index bf12b1d..6b3ee8e 100644 --- a/app/components/Starknet-provider.tsx +++ b/app/dapp/components/Starknet-provider.tsx @@ -1,6 +1,6 @@ "use client"; import React from "react"; - + import { sepolia, mainnet } from "@starknet-react/chains"; import { StarknetConfig, @@ -8,31 +8,28 @@ import { argent, braavos, useInjectedConnectors, - voyager + voyager, } from "@starknet-react/core"; - -export function StarknetProvider({ children }: { children: React.ReactNode }) { +const StarknetProvider = ({ children }: { children: React.ReactNode }) => { const { connectors } = useInjectedConnectors({ // Show these connectors if the user has no connector installed. - recommended: [ - argent(), - braavos(), - ], + recommended: [argent(), braavos()], // Hide recommended connectors if the user has any connector installed. includeRecommended: "onlyIfNoConnectors", // Randomize the order of the connectors. - order: "random" + order: "random", }); - + return ( + explorer={voyager}> {children} ); -} \ No newline at end of file +} + +export default StarknetProvider; \ No newline at end of file diff --git a/app/dapp/components/analytics/AssetPieChart.tsx b/app/dapp/components/analytics/AssetPieChart.tsx new file mode 100644 index 0000000..0f70c69 --- /dev/null +++ b/app/dapp/components/analytics/AssetPieChart.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { Lock } from "lucide-react"; + +const assetData = [ + { name: 'USDT', value: 3193.21, percentage: 45.4, color: '#2F80ED', price: 1.00 }, + { name: 'ETH', value: 2931.49, percentage: 21.7, color: '#27AE60', price: 3245.67 }, + { name: 'SOL', value: 938.84, percentage: 19.3, color: '#F2994A', price: 98.45 }, + { name: 'STRK', value: 411.32, percentage: 8.1, color: '#EB5757', price: 1.23 }, + { name: 'FTM', value: 221.32, percentage: 5.5, color: '#9B51E0', price: 0.45 } +]; + +export default function AssetPieChart() { + return ( +
    +
    +

    + Your Locked Assets +

    +
    + +
    + {/* First Row - Chart and Legend */} +
    + {/* Left Column - Donut Chart with Lock Icon */} +
    + + + + {assetData.map((entry, index) => ( + + ))} + + + + +
    +
    + +
    +
    +
    + + {/* Right Column - Legend */} +
    + {assetData.map((asset, index) => ( +
    +
    + + {asset.name} + +
    + ))} +
    +
    + + {/* Second Row - Asset Details */} +
    + {assetData.map((asset, index) => ( +
    +
    + + {asset.name} + +
    +
    +
    + ${asset.value.toLocaleString()} +
    +
    + {asset.percentage}% +
    +
    +
    + ))} +
    +
    +
    + ); +} \ No newline at end of file diff --git a/app/dapp/components/analytics/ChartCard.tsx b/app/dapp/components/analytics/ChartCard.tsx new file mode 100644 index 0000000..2dc8ab3 --- /dev/null +++ b/app/dapp/components/analytics/ChartCard.tsx @@ -0,0 +1,239 @@ +"use client"; + +import type React from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + CartesianGrid, +} from "recharts"; +import { useTranslation } from "react-i18next"; +import "../../../i18n-client"; // Initialize i18n on client side + +interface ChartCardProps { + selectedChart: "tvl" | "volume" | "price"; + onChartChange: (chart: "tvl" | "volume" | "price") => void; + theme: string; + currentPrice?: string; + priceChange?: string; +} + +// Small logo component specifically for ChartCard +function SmallLogo({ className, ...props }: React.SVGProps) { + return ( + + + + + + + + + + + + ); +} + +// Mock data for different chart types with the exact dates and values +const chartData = { + tvl: [ + { time: "Jan 25", value: 250000 }, + { time: "Jan 26", value: 100000 }, + { time: "Jan 27", value: 10000 }, + { time: "Jan 28", value: 5000 }, + { time: "Jan 29", value: 1000 }, + { time: "Jan 30", value: 1000 }, + { time: "Jan 31", value: 150000 }, + { time: "Feb 1", value: 150000 }, + ], + volume: [ + { time: "Jan 25", value: 250000 }, + { time: "Jan 26", value: 100000 }, + { time: "Jan 27", value: 10000 }, + { time: "Jan 28", value: 5000 }, + { time: "Jan 29", value: 1000 }, + { time: "Jan 30", value: 1000 }, + { time: "Jan 31", value: 50000 }, + { time: "Feb 1", value: 150000 }, + ], + price: [ + { time: "Jan 25", value: 250000 }, + { time: "Jan 26", value: 100000 }, + { time: "Jan 27", value: 10000 }, + { time: "Jan 28", value: 5000 }, + { time: "Jan 29", value: 1000 }, + { time: "Jan 30", value: 0 }, + { time: "Jan 31", value: 50000 }, + { time: "Feb 1", value: 150000 }, + ], +}; + +export default function ChartCard({ + selectedChart, + onChartChange, + theme, + currentPrice, + priceChange, +}: ChartCardProps) { + const { t } = useTranslation(); + + const chartConfig = { + tvl: { + label: t("analytics.tvl"), + color: "#8884d8", + format: (value: number) => `$${(value / 1000).toFixed(0)}K`, + }, + volume: { + label: t("analytics.volume"), + color: "#82ca9d", + format: (value: number) => `$${(value / 1000).toFixed(0)}K`, + }, + price: { + label: t("analytics.price"), + color: "#ffc658", + format: (value: number) => `$${(value / 1000).toFixed(0)}K`, + }, + }; + + const lineColor = theme === "dark" ? "#fff" : "#000"; + console.log("ChartCard theme:", theme, "lineColor:", lineColor); + const axisAndGridColor = theme === "dark" ? "#444" : "#e5e5e5"; + const textColor = theme === "dark" ? "#999" : "#999"; + + const currentData = chartData[selectedChart]; + const config = chartConfig[selectedChart]; + + return ( +
    +
    +
    +
    +
    + +
    +

    + ZeroXBridge (xZB) +

    +
    +
    +

    + {currentPrice || "$1.1392"} +

    + + {priceChange || "+2.38%"} + +
    +
    +
    + {(["tvl", "volume", "price"] as const).map((chart) => ( + + ))} +
    +
    +
    + + + + + { + if (value === 0) return "0"; + if (value >= 100000) return `$${(value / 1000).toFixed(0)}K`; + if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`; + return `$${value}`; + }} + domain={["dataMin", "dataMax"]} + tickCount={6} + /> + [ + config.format(value), + config.label, + ]} + labelStyle={{ color: theme === "dark" ? "#fff" : "#000" }} + contentStyle={{ + backgroundColor: theme === "dark" ? "#1a1a1a" : "#fff", + border: `1px solid ${theme === "dark" ? "#333" : "#e5e5e5"}`, + borderRadius: "8px", + fontSize: "12px", + }} + /> + + + +
    +
    + ); +} diff --git a/app/dapp/components/analytics/EmptyState.tsx b/app/dapp/components/analytics/EmptyState.tsx new file mode 100644 index 0000000..7762190 --- /dev/null +++ b/app/dapp/components/analytics/EmptyState.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { PieChartIcon } from "lucide-react"; +import { ConnectWalletButton } from "../ui/ConnectWalletButton"; +import { useTranslation } from "react-i18next"; +import "../../../i18n-client"; // Initialize i18n on client side + +export default function EmptyState() { + const { t } = useTranslation(); + return ( +
    + {/* Page Header */} +
    +

    + {t("navigation.analytics")} +

    +

    + {t("wallet.connect")} {t("common.amount")} {t("analytics.tvl")}{" "} + {t("analytics.totalValueLocked")} +

    +
    + + {/* Empty State */} +
    +
    +
    + +
    + +
    +

    + {t("wallet.connect")} +

    +

    + {t("analytics.tvl")} {t("analytics.totalValueLocked")}{" "} + {t("analytics.lockedAssets")} {t("analytics.xzbBalance")}{" "} + {t("analytics.change24h")} +

    +
    + + +
    +
    +
    + ); +} diff --git a/app/dapp/components/analytics/StatsOverview.tsx b/app/dapp/components/analytics/StatsOverview.tsx new file mode 100644 index 0000000..de7f029 --- /dev/null +++ b/app/dapp/components/analytics/StatsOverview.tsx @@ -0,0 +1,41 @@ +"use client"; + +interface Stat { + id: string; + title: string; + value: string; +} + +interface StatsOverviewProps { + stats: Stat[]; +} + +export default function StatsOverview({ stats }: StatsOverviewProps) { + return ( +
    + {stats.map((stat) => ( +
    +
    +

    + {stat.title} +

    +

    + {stat.value} +

    +
    +
    + ))} +
    + ); +} diff --git a/app/dapp/components/claim-burn/index.tsx b/app/dapp/components/claim-burn/index.tsx new file mode 100644 index 0000000..55d95fb --- /dev/null +++ b/app/dapp/components/claim-burn/index.tsx @@ -0,0 +1,291 @@ +"use client"; + +import { useWallet } from "@/app/hooks/useWallet"; +import { useMemo, useState } from "react"; +import { SuccessModal } from "../../../dapp/claim-burn/components/success"; +import { ConnectWalletButton } from "../ui/ConnectWalletButton"; +import Image from "next/image"; +import { ClaimBurnTab } from "../../../dapp/claim-burn/components/tab"; +import { Geist_Mono, Inter } from "next/font/google"; +import { Hamburger } from "@/svg/Hamburger"; +import { Info } from "@/svg/Info"; +import { useThemeContext } from "@/app/hooks/context"; +import { useTranslation } from "react-i18next"; +import "../../../i18n-client"; // Initialize i18n on client side + +const geistMono = Geist_Mono({ + subsets: ["latin"], +}); + +const inter = Inter({ + subsets: ["latin"], +}); + +type BurnClaimData = { + available: number; + value: string; + price: string; + fee: string; + displayAmount: string; +}; + +const ClaimBurn = () => { + const { isDark } = useThemeContext(); + const { isConnected } = useWallet(); + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState("claim"); + const [amount, setAmount] = useState(""); + const [showSuccessModal, setShowSuccessModal] = useState(false); + + const CLAIM_BURN_DATA: Record = useMemo( + () => ({ + claim: { + available: isConnected ? 3939 : 0, + value: isConnected ? "$3394.13" : "--", + price: isConnected ? "$0.123" : "--", + fee: "$0", + displayAmount: isConnected ? "3094.00" : "0.00", + }, + burn: { + available: isConnected ? 3939 : 0, + value: isConnected ? "$3394.13" : "--", + price: isConnected ? "$0.123" : "--", + fee: "$0", + displayAmount: isConnected ? "3094.00" : "0.00", + }, + }), + [isConnected] + ); + + const currentData = CLAIM_BURN_DATA[activeTab]; + + const handleMaxClick = () => { + if (isConnected) setAmount(currentData.displayAmount); + }; + + const handleAction = () => { + if (isConnected && amount) setShowSuccessModal(true); + }; + + const isActionable = !!(amount && amount !== "0" && amount !== "0.00"); + const claimBurnBtnClasses = isActionable + ? isDark + ? "bg-white text-[#515151] hover:opacity-80" + : "bg-black text-white hover:opacity-80" + : isDark + ? "bg-[#2e2e2e] text-[#515151] cursor-not-allowed" + : "bg-[#f0f0f0] text-[#c4c4c4] cursor-not-allowed"; + + return ( + <> +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + ZeroXBridge Logo +
    +
    +

    + {activeTab === "claim" + ? t("claimBurn.claimXZB") + : t("claimBurn.burnXZB")} +

    +

    xZB

    +
    +
    +
    +
    + setAmount(e.target.value)} + disabled={!isConnected} + className={`no-spinner w-full py-4 text-4xl font-light bg-transparent outline-none border-none pr-20 ${ + isDark + ? "text-white placeholder-[#515151]" + : "text-[var(--claim-area)] placeholder-[var(--claim-input-placeholder)]" + } ${geistMono.className}`} + /> + +
    + + +
    + +
    +
    + + + {activeTab === "burn" && isConnected && amount && ( + + )} +
    + + {activeTab === "burn" && ( +
    + +

    + {t("claimBurn.burnFirstWarning")} +

    +
    + )} + + {!isConnected ? ( + + ) : ( + + )} +
    +
    +
    +
    +
    + setShowSuccessModal(false)} + type={activeTab} + amount={amount} + /> + + ); +}; + +interface InfoRowProps { + label: string; + value: string; + isDark: boolean; + valueFontWeight?: string; +} + +const InfoRow = ({ label, value, isDark, valueFontWeight }: InfoRowProps) => ( +
    + + {label} + + + {value} + +
    +); + +export default ClaimBurn; diff --git a/app/dapp/components/layout/AppLayout.tsx b/app/dapp/components/layout/AppLayout.tsx new file mode 100644 index 0000000..af8027e --- /dev/null +++ b/app/dapp/components/layout/AppLayout.tsx @@ -0,0 +1,53 @@ +"use client"; + +import React, { useState } from "react"; +import Topbar from "./Topbar"; +import Sidebar from "./Sidebar"; +import { ConnectWalletModal } from "../../dashboard/components/connect-wallet"; +import { usePathname } from "next/navigation"; +import { ComingSoonFooter } from "./Footer"; + +interface AppLayoutProps { + children: React.ReactNode; + /** the comming soon page has a different layout interms of spacing + * the gradient banner touches the edges of it parent, the app layout. + * with the current setup/style of the AppLayout, we need to have an optional prop + * to help ud sepcifiy whether to use the default padding or not. + */ + layoutPadding?: boolean; +} + +function AppLayout({ children, layoutPadding = true }: AppLayoutProps) { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const pathname = usePathname(); + const isComingSoon = pathname === "/dapp/coming-soon"; + + return ( + <> +
    + setIsSidebarOpen((prev) => !prev)} + isSidebarOpen={isSidebarOpen} + /> +
    + setIsSidebarOpen(false)} + /> +
    + {children} + + {isComingSoon && } +
    +
    +
    + + + ); +} + +export default AppLayout; diff --git a/app/dapp/components/layout/Footer.tsx b/app/dapp/components/layout/Footer.tsx new file mode 100644 index 0000000..e89cdb7 --- /dev/null +++ b/app/dapp/components/layout/Footer.tsx @@ -0,0 +1,26 @@ +import { MoveRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import "../../../i18n-client"; + +export const ComingSoonFooter = () => { + const { t } = useTranslation(); + return ( +
    +
    +
    + {t("joinCommunity.title")} 🔔 +
    +
    + + +
    +
    +
    + ); +}; diff --git a/app/dapp/components/layout/Sidebar.tsx b/app/dapp/components/layout/Sidebar.tsx new file mode 100644 index 0000000..fc72607 --- /dev/null +++ b/app/dapp/components/layout/Sidebar.tsx @@ -0,0 +1,125 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import HomeIcon from "@/svg/HomeIcon"; +import SwapIcon from "@/svg/SwapIcon"; +import CryptoIcon from "@/svg/CryptoIcon"; +import LockIcon from "@/svg/LockIcon"; +import PieChartIcon from "@/svg/PieChartIcon"; +import HourglassIcon from "@/svg/HourglassIcon"; +import SettingsIcon from "@/svg/SettingsIcon"; +import { useTranslation } from "react-i18next"; +import LanguageSwitcher from "../../../components/LanguageSwitcher"; +import "../../../i18n-client"; + +function Sidebar({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const pathname = usePathname(); + const { t } = useTranslation(); + + const routes = [ + { + label: t("navigation.dashboard"), + href: "/dapp/dashboard", + icon: HomeIcon, + }, + { label: t("navigation.swap"), href: "/dapp/swap", icon: SwapIcon }, + { + label: t("navigation.claimBurn"), + href: "/dapp/claim-burn", + icon: CryptoIcon, + }, + { + label: t("navigation.lockTokens"), + href: "/dapp/lock-tokens", + icon: LockIcon, + }, + { + label: t("navigation.analytics"), + href: "/dapp/analytics", + icon: PieChartIcon, + }, + { label: "Coming Soon", href: "/dapp/coming-soon", icon: HourglassIcon }, + ]; + + return ( +
    +
    + {routes.map(({ label, href, icon: Icon }) => { + const isActive = pathname === href; + return ( + +
    + + {label} +
    + + + ); + })} +
    + +
    + +
    + + {t("navigation.settings")} +
    + + + {/* Language switcher for mobile only */} +
    +
    + + {t("language.switchLanguage")} + + +
    +
    +
    +
    + ); +} + +export default Sidebar; diff --git a/app/dapp/components/layout/Topbar.tsx b/app/dapp/components/layout/Topbar.tsx new file mode 100644 index 0000000..79755b0 --- /dev/null +++ b/app/dapp/components/layout/Topbar.tsx @@ -0,0 +1,54 @@ +import Logo from "../ui/Logo"; +import ThemeSwitcher from "../ui/ThemeSwitcher"; +import { ConnectWalletButton } from "../ui/ConnectWalletButton"; +import HamburgerIcon from "@/svg/HamburgerIcon"; +import { X } from "lucide-react"; +import { useWallet } from "@/app/hooks"; +import { shortenAddress } from "@/lib/utils"; +import LanguageSwitcher from "../../../components/LanguageSwitcher"; + +function Topbar({ + onMenuClick, + isSidebarOpen, +}: { + onMenuClick: () => void; + isSidebarOpen?: boolean; +}) { + const { + openWalletModal, + ethConnected, + strkConnected, + strkAddress, + ethAddress, + } = useWallet(); + const isWalletConnected = strkConnected || ethConnected; + const walletAddress = ethAddress || strkAddress; + return ( +
    +
    + + +
    +
    + {/* Language switcher only on desktop/4K */} +
    + +
    + + +
    +
    + ); +} + +export default Topbar; diff --git a/app/dapp/components/lock-tokens.tsx b/app/dapp/components/lock-tokens.tsx new file mode 100644 index 0000000..f474e8d --- /dev/null +++ b/app/dapp/components/lock-tokens.tsx @@ -0,0 +1,358 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Menu, Info } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent } from '@/components/ui/card' +import { TokenSelectDropdown } from '@/app/dapp/components/token-select-dropdown' +import { SuccessModal } from '@/app/dapp/components/success-modal' +import type { Token, LockTransaction } from '@/types/token' +import { ConnectWalletButton } from './ui/ConnectWalletButton' +import { useWallet } from '@/app/hooks' +import { useTranslation } from 'react-i18next' +import '../../i18n-client' // Initialize i18n on client side + +// Mock token data +const mockTokens: Token[] = [ + { + symbol: 'ETH', + name: 'Ethereum', + icon: '⟠', + price: 3193.21, + liquidity: 1391195483.0, + xzbRate: 1.29, + riskLevel: 'Low Risk', + balance: 391.12, + }, + { + symbol: 'BTC', + name: 'Bitcoin', + icon: '₿', + price: 45000.0, + liquidity: 2500000000.0, + xzbRate: 1.15, + riskLevel: 'Low Risk', + balance: 2.5, + }, + { + symbol: 'USDC', + name: 'USD Coin', + icon: '$', + price: 1.0, + liquidity: 500000000.0, + xzbRate: 0.95, + riskLevel: 'High Risk', + balance: 10000.0, + }, +] + +function SwapFromInputSkeleton() { + return ( +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + ) +} + +function SwapQuoteDetailsSkeleton() { + return ( +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + ) +} + +const TokenLockInterface = () => { + const [selectedToken, setSelectedToken] = useState(null) + const [amount, setAmount] = useState('') + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false) + const [lastTransaction, setLastTransaction] = + useState(null) + const [loadingToken, setLoadingToken] = useState(true) + + const { isConnected, openWalletModal } = useWallet() + const { t } = useTranslation() + + const handleTokenSelect = (token: Token) => { + setSelectedToken(token) + } + + const handleMaxClick = () => { + if (selectedToken) { + setAmount(selectedToken.balance.toString()) + } + } + + const handleLock = () => { + if (selectedToken && amount) { + const numAmount = Number.parseFloat(amount) + const xzbReceived = numAmount * selectedToken.xzbRate + + const transaction: LockTransaction = { + amount: numAmount, + token: selectedToken, + xzbReceived, + timestamp: new Date().toLocaleString('en-US', { + day: '2-digit', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true, + }), + txHash: '0x' + Math.random().toString(16).substr(2, 64), + } + + setLastTransaction(transaction) + setIsSuccessModalOpen(true) + setAmount('') + } + } + + const calculateXzbReceived = () => { + if (selectedToken && amount) { + return (Number.parseFloat(amount) * selectedToken.xzbRate).toFixed(0) + } + return '0' + } + + useEffect(() => { + const timer = setTimeout(() => { + setLoadingToken(false) + }, 3000) + + return () => clearTimeout(timer) + }, []) + + return ( +
    +
    + +
    + +
    + + + {loadingToken ? ( + <> + + + + ) : ( + <> +
    + + +
    +
    + setAmount(e.target.value)} + placeholder={t('deposit.enterAmount')} + className={`!text-4xl no-spinner font-light border-none !outline-none ring-0 + focus:ring-0 focus:outline-none focus:shadow-none + p-0 h-12 !bg-transparent + dark:text-[#FFF] dark:placeholder-gray-500 + placeholder-gray-400`} + disabled={!isConnected || !selectedToken} + /> + + +
    + {isConnected && ( + <> +
    +
    + + {t('deposit.balance')}: + + + {selectedToken + ? `${selectedToken.balance} ${selectedToken.symbol}` + : '-- xZB'} + +
    + + )} +
    +
    + +
    +
    + + Token Price: + + + {selectedToken + ? `$${selectedToken.price.toLocaleString()}` + : '$--'} + +
    + +
    + + Current Liquidity + + + {selectedToken + ? `$${selectedToken.liquidity.toLocaleString()}` + : '$--'} + +
    + +
    + + xZB Token Rate: + + + {selectedToken ? `$${selectedToken.xzbRate}` : '$--'} + +
    + + {selectedToken && amount && ( +
    + + {"You'll receive:"} + + + {calculateXzbReceived()} xZB + +
    + )} +
    + + )} + {isConnected && selectedToken && ( +
    +
    + +

    + {t('lockSummary.title')} {selectedToken.symbol}{' '} + {t('common.amount')}, {t('claimBurn.availableToClaim')} xZB + {t('claimBurn.burnXZB')} {t('claimBurn.availableToClaim')}{' '} + {selectedToken.symbol}. +

    +
    +
    + )} + + {!loadingToken && ( +
    + {isConnected ? ( + + ) : ( + + )} +
    + )} + + +
    + + setIsSuccessModalOpen(false)} + transaction={lastTransaction} + /> +
    + ) +} + +export default TokenLockInterface diff --git a/app/dapp/components/success-modal.tsx b/app/dapp/components/success-modal.tsx new file mode 100644 index 0000000..78650ca --- /dev/null +++ b/app/dapp/components/success-modal.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { Globe } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import type { LockTransaction } from "@/types/token"; +import { useThemeContext } from "@/app/context/theme-provider"; +import Check from "@/public/check.png"; +import CheckDark from "@/public/check-dark.png"; +import Image from "next/image"; +import { useTranslation } from "react-i18next"; +import "../../i18n-client"; // Initialize i18n on client side + +interface SuccessModalProps { + isOpen: boolean; + onClose: () => void; + transaction: LockTransaction | null; +} + +export function SuccessModal({ + isOpen, + onClose, + transaction, +}: SuccessModalProps) { + const { isDark } = useThemeContext(); + const { t } = useTranslation(); + if (!transaction) return null; + + return ( + + +
    +
    + Check Icon +
    + +
    +

    + {transaction.token.symbol} {t("lockSummary.title")}! +

    +

    + {t("lockSummary.title")} {transaction.amount}{" "} + {transaction.token.symbol} + {t("claimBurn.availableToClaim")}{" "} + {transaction.xzbReceived.toFixed(2)} xZB! +

    +
    + +

    + {transaction.timestamp} +

    + +
    + + +
    +
    +
    +
    + ); +} diff --git a/app/dapp/components/token-select-dropdown.tsx b/app/dapp/components/token-select-dropdown.tsx new file mode 100644 index 0000000..07452cd --- /dev/null +++ b/app/dapp/components/token-select-dropdown.tsx @@ -0,0 +1,132 @@ +'use client' +import { ChevronDown } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import type { Token } from '@/types/token' +import { useTranslation } from 'react-i18next' +import '../../i18n-client' // Initialize i18n on client side +// import { useTheme } from "@/app/hooks/useTheme"; + +interface TokenSelectDropdownProps { + selectedToken: Token | null + onTokenSelect: (token: Token) => void + tokens: Token[] + loadingToken?: boolean +} + +export function TokenSelectDropdown({ + selectedToken, + onTokenSelect, + tokens, +}: TokenSelectDropdownProps) { + const { t } = useTranslation() + return ( + + + + + + {tokens.map((token) => ( + onTokenSelect(token)} + className={`flex items-center gap-3 p-3 dark:text-white hover:bg-gray-50 text-gray-900`} + > +
    + + {token.symbol.slice(0, 2)} + +
    +
    +
    {token.symbol}
    +
    + {token.name} +
    +
    +
    + + {token.riskLevel} + +
    +
    + ))} +
    +
    + ) +} diff --git a/app/dapp/components/ui/AddressBar.tsx b/app/dapp/components/ui/AddressBar.tsx new file mode 100644 index 0000000..f1e92fb --- /dev/null +++ b/app/dapp/components/ui/AddressBar.tsx @@ -0,0 +1,39 @@ +import { useConnection } from "@/app/context/ConnectionContext"; +import DisconnectIcon from "@/svg/DisconnectIcon"; +import WalletIcon from "@/svg/WalletIcon"; + +function AddressBar({ className }: { className?: string }) { + const { walletAddress, setIsConnected } = useConnection(); + + const shortenedAddress = walletAddress + ? `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}` + : "Not connected"; + + // ✅ define handleDisconnect + const handleDisconnect = () => { + setIsConnected(false); + }; + + + return ( +
    + + {shortenedAddress} + +
    + +
    + ); +} + +export default AddressBar; diff --git a/app/dapp/components/ui/ArrowIcon.tsx b/app/dapp/components/ui/ArrowIcon.tsx new file mode 100644 index 0000000..d661a0f --- /dev/null +++ b/app/dapp/components/ui/ArrowIcon.tsx @@ -0,0 +1,35 @@ +const ArrowIcon = ({ + direction, + ...svgProps +}: React.SVGProps & { + direction: "left" | "right" | "up" | "down"; +}) => { + return ( + + + + ); +}; + +export default ArrowIcon; diff --git a/app/dapp/components/ui/ConnectWalletButton.tsx b/app/dapp/components/ui/ConnectWalletButton.tsx new file mode 100644 index 0000000..52f99cc --- /dev/null +++ b/app/dapp/components/ui/ConnectWalletButton.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { BrokenLink } from "@/svg/BrokenLink"; +import { Spinner } from "@/svg/Spinner"; +import WalletIcon from "@/svg/WalletIcon"; +import { GradientDirection, GradientWrapperPrimary } from "./Gradients"; +import { useThemeContext } from "@/app/hooks/context"; +import { useEffect, useMemo } from "react"; +import { toast } from "sonner"; +import { useWallet } from "@/app/hooks/useWallet"; +import { useTranslation } from "react-i18next"; +import "../../../i18n-client"; // Initialize i18n on client side + +interface ConnectWalletButtonProps { + full?: boolean; + className?: string; + /** Toggle gradient border wrapping */ + withGradient?: boolean; + /** The direction of the gradient */ + gradientDirection?: GradientDirection; + /** Make button thinner with reduced padding */ + thin?: boolean; + /** Action of the button. If not provided, will open wallet modal by default */ + action?: () => void; + isLoading?: boolean; + isConnected?: boolean; + walletAddress?: string | null; + error?: string | null; + /** prop to show the broken link icon */ + showBrokenLink?: boolean; +} + +export const ConnectWalletButton = ({ + full, + className = "", + withGradient = true, + gradientDirection, + thin = false, + action, + isLoading, + isConnected, + walletAddress, + error, + showBrokenLink, +}: ConnectWalletButtonProps) => { + const { t } = useTranslation(); + const { isDark } = useThemeContext(); + + // Pull defaults from the wallet hook, but allow props to override + const wallet = useWallet(); // { isConnected, openWalletModal, ... } + const resolvedIsConnected = useMemo( + () => isConnected ?? wallet?.isConnected ?? false, + [isConnected, wallet?.isConnected] + ); + const resolvedWalletAddress = useMemo( + () => walletAddress ?? wallet?.ethAddress ?? wallet?.strkAddress ?? null, + [walletAddress, wallet?.ethAddress, wallet?.strkAddress] + ); + const resolvedIsLoading = useMemo( + () => isLoading ?? wallet?.ethConnecting ?? wallet?.strkConnecting ?? false, + [isLoading, wallet?.ethConnecting, wallet?.strkConnecting] + ); + const resolvedError = useMemo( + () => error ?? wallet?.error ?? null, + [error, wallet?.error] + ); + + useEffect(() => { + if (resolvedError) toast.error(`${t("common.error")}: ${resolvedError}`); + }, [resolvedError, t]); + + const handleClick = () => { + // Use provided action if any; otherwise open the wallet modal from the hook + const fallback = wallet?.openWalletModal; + const fn = action ?? fallback; + if (fn) fn(); + }; + + const getPaddingAndRoundness = () => { + if (thin) return "px-3 py-1 rounded-[6px]"; + if (full) return "py-4 rounded-[8px]"; + return "py-2 rounded-[8px]"; + }; + + const baseClasses = `flex items-center justify-center px-3 ${getPaddingAndRoundness()} text-primary-text border border-wallet-border transition-all duration-200 hover:opacity-80 active:opacity-60 ${ + full ? "w-full" : "" + } ${className}`; + + const getButtonContent = () => { + if (resolvedIsLoading) { + return ( +
    + + {t("wallet.connecting")} +
    + ); + } + + if (resolvedIsConnected) { + return ( +
    +
    + + + {resolvedWalletAddress} + +
    + {showBrokenLink && ( +
    + +
    + )} +
    + ); + } + + return ( +
    + + + {t("wallet.connect")} + +
    + ); + }; + + const ButtonContent = ( + + ); + + return withGradient ? ( + + {ButtonContent} + + ) : ( + ButtonContent + ); +}; diff --git a/app/dapp/components/ui/Dailog.tsx b/app/dapp/components/ui/Dailog.tsx new file mode 100644 index 0000000..2852a8e --- /dev/null +++ b/app/dapp/components/ui/Dailog.tsx @@ -0,0 +1,68 @@ +"use client"; + +import * as Dialog from "@radix-ui/react-dialog"; +import { ReactNode, useMemo } from "react"; +import { useThemeContext } from "@/app/hooks/context"; +import { Close } from "@/svg/CloseIcon"; + +interface DialogBaseProps { + isOpen: boolean; + onClose: () => void; + children: ReactNode; + size?: "sm" | "md" | "lg" | "xl"; + className?: string; + title?: string; + /* helps you decided whether to give the modal a close button or not */ + addCloseBtn?: boolean; +} + +const SIZE_MAP = { + sm: "w-[90%] max-w-sm sm:min-w-[18rem]", + md: "w-[90%] max-w-md sm:min-w-[22rem]", + lg: "w-[90%] max-w-lg sm:min-w-[28rem]", + xl: "w-[90%] max-w-xl sm:min-w-[34rem]", +}; + +export const DialogBase = ({ + isOpen, + onClose, + children, + size = "md", + className = "", + title, + addCloseBtn, +}: DialogBaseProps) => { + const { theme } = useThemeContext(); + const isDark = useMemo(() => theme === "dark", [theme]); + return ( + !open && onClose()}> + + + + {addCloseBtn && ( + + + + )} + {title && ( + + {title} + + )} + {children} + + + + ); +}; diff --git a/app/dapp/components/ui/Gradients.tsx b/app/dapp/components/ui/Gradients.tsx new file mode 100644 index 0000000..3e54b6c --- /dev/null +++ b/app/dapp/components/ui/Gradients.tsx @@ -0,0 +1,50 @@ +// across the design we have these occurences of gradient-like +// patterns on elements appearing like a border. +// in this file, we'll keep a couple of them here. +// either in dark or light theme +export type GradientDirection = "to-right" | "to-left" | "to-top" | "to-bottom"; + +interface GradientWrapperProps { + children: React.ReactNode; + className?: string; + borderRadius?: string; + padding?: string; + /** The direction of the gradient. + * Can be: "to-right" | "to-left" | "to-top" | "to-bottom" + */ + gradientDirection?: GradientDirection; +} + +export const GradientWrapperPrimary = ({ + children, + className = "", + borderRadius = "rounded-xl", + padding = "p-[2px]", + gradientDirection = "to-right", +}: GradientWrapperProps) => { + let gradientClass = "bg-gradient-to-r"; + + switch (gradientDirection) { + case "to-left": + gradientClass = "bg-gradient-to-l"; + break; + case "to-top": + gradientClass = "bg-gradient-to-t"; + break; + case "to-bottom": + gradientClass = "bg-gradient-to-b"; + break; + case "to-right": + default: + gradientClass = "bg-gradient-to-r"; + break; + } + + return ( +
    + {children} +
    + ); +}; diff --git a/app/dapp/components/ui/Logo.tsx b/app/dapp/components/ui/Logo.tsx new file mode 100644 index 0000000..731c6b9 --- /dev/null +++ b/app/dapp/components/ui/Logo.tsx @@ -0,0 +1,11 @@ +import LogoIcon from "@/svg/LogoIcon"; + +function Logo() { + return ( +
    + +
    + ); +} + +export default Logo; diff --git a/app/dapp/components/ui/Preloader.tsx b/app/dapp/components/ui/Preloader.tsx new file mode 100644 index 0000000..26bdbab --- /dev/null +++ b/app/dapp/components/ui/Preloader.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useTheme } from "next-themes"; + +interface PreloaderProps { + onComplete: () => void; +} + +export default function Preloader({ onComplete }: PreloaderProps) { + const [progress, setProgress] = useState(0); + const [animationPhase, setAnimationPhase] = useState<"loading" | "exiting">("loading"); + const [logoRotation, setLogoRotation] = useState(0); + const [logoScale, setLogoScale] = useState(1); + const { theme } = useTheme(); + + const handleComplete = useCallback(() => { + onComplete(); + }, [onComplete]); + + useEffect(() => { + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + if (prefersReducedMotion) { + setProgress(100); + const timer = setTimeout(() => { + handleComplete(); + }, 1000); + return () => clearTimeout(timer); + } + + const animateLogoRotation = (newRotation: number, isLastRotation = false) => { + if (isLastRotation) { + // Final rotation: -25 degrees then back to 0 + setLogoRotation(-25); + setLogoScale(1.3); + setTimeout(() => { + setLogoRotation(0); + setLogoScale(1); + }, 300); + } else { + setLogoRotation(newRotation); + setLogoScale(1.3); + setTimeout(() => setLogoScale(1), 200); + } + }; + + const progressStages = [ + { progress: 30, delay: 1000 }, + { progress: 89, delay: 1000 }, + { progress: 100, delay: 1500 } + ]; + + const timeoutIds: NodeJS.Timeout[] = []; + let totalDelay = 0; + + progressStages.forEach((stage, index) => { + totalDelay += stage.delay; + const timeoutId = setTimeout(() => { + setProgress(stage.progress); + const isLastRotation = stage.progress === 100; + animateLogoRotation((index + 1) * 90, isLastRotation); + + if (stage.progress === 100) { + setTimeout(() => { + setAnimationPhase("exiting"); + setTimeout(() => handleComplete(), 300); + }, 800); + } + }, totalDelay); + timeoutIds.push(timeoutId); + }); + + return () => { + timeoutIds.forEach(clearTimeout); + }; + }, [handleComplete]); + + + const logoColor = theme === "dark" ? "#FFFFFF" : "#ffffff"; + + return ( + +