From 638d6116f704208bc31b5518be0239e02804fcb4 Mon Sep 17 00:00:00 2001 From: manideep1428 Date: Tue, 24 Sep 2024 16:17:14 +0530 Subject: [PATCH] redesign --- app/(pages)/layout.tsx | 16 + app/(pages)/markets/page.tsx | 147 ++++++ app/(pages)/trade/[market]/page.tsx | 110 +++++ app/api/auth/[...nextauth]/route.ts | 76 +-- app/auth/error/page.tsx | 7 - app/auth/signIn/page.tsx | 9 - app/auth/signup/page.tsx | 79 ---- app/layout.tsx | 3 +- app/markets/page.tsx | 8 - app/page.tsx | 229 ++++----- app/test/page.tsx | 146 +----- app/trade/[market]/page.tsx | 46 -- app/utils/Chart.ts | 168 +++++++ app/utils/ServerProps.ts | 11 +- app/utils/types.ts | 32 +- components/Appbar.tsx | 158 ++++--- components/CryptoTable.tsx | 52 +++ components/MarketBar.tsx | 126 ++--- components/OrderUI.tsx | 321 +++++++------ components/SiginBase.tsx | 128 ----- components/SignInForm.tsx | 55 +++ components/SignUpForm.tsx | 71 +++ components/SignupBase.tsx | 67 --- components/Skeletons/AskBidSkeleton.tsx | 27 ++ components/Skeletons/MarketBarSkeleton.tsx | 28 ++ components/Skeletons/TradingViewSkeleton.tsx | 22 + components/TradeView.tsx | 309 +++++++----- components/Wallet.tsx | 307 ++++++------ components/depth/AskTable.tsx | 84 ++-- components/depth/BidTable.tsx | 79 ++-- components/depth/Depth.tsx | 92 ---- components/ui/alert.tsx | 59 +++ components/ui/switch.tsx | 29 ++ components/ui/tabs.tsx | 55 +++ .../20240916113127_init56/migration.sql | 2 + .../20240922143806_init34/migration.sql | 62 +++ db/prisma/schema.prisma | 107 +++-- hooks/MarketWebsockets.ts | 27 ++ lib/auth.ts | 16 + lib/prisma.ts | 7 + lib/types.ts | 10 + lib/utils.ts | 9 +- package-lock.json | 440 +++++++++++++++++- package.json | 9 +- 44 files changed, 2353 insertions(+), 1492 deletions(-) create mode 100644 app/(pages)/layout.tsx create mode 100644 app/(pages)/markets/page.tsx create mode 100644 app/(pages)/trade/[market]/page.tsx delete mode 100644 app/auth/error/page.tsx delete mode 100644 app/auth/signIn/page.tsx delete mode 100644 app/auth/signup/page.tsx delete mode 100644 app/markets/page.tsx delete mode 100644 app/trade/[market]/page.tsx create mode 100644 app/utils/Chart.ts create mode 100644 components/CryptoTable.tsx delete mode 100644 components/SiginBase.tsx create mode 100644 components/SignInForm.tsx create mode 100644 components/SignUpForm.tsx delete mode 100644 components/SignupBase.tsx create mode 100644 components/Skeletons/AskBidSkeleton.tsx create mode 100644 components/Skeletons/MarketBarSkeleton.tsx create mode 100644 components/Skeletons/TradingViewSkeleton.tsx delete mode 100644 components/depth/Depth.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 db/prisma/migrations/20240916113127_init56/migration.sql create mode 100644 db/prisma/migrations/20240922143806_init34/migration.sql create mode 100644 hooks/MarketWebsockets.ts create mode 100644 lib/prisma.ts create mode 100644 lib/types.ts diff --git a/app/(pages)/layout.tsx b/app/(pages)/layout.tsx new file mode 100644 index 0000000..7db1cf6 --- /dev/null +++ b/app/(pages)/layout.tsx @@ -0,0 +1,16 @@ +import { Appbar } from '@/components/Appbar' +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + + return ( + + + + {children} + + + ) +} \ No newline at end of file diff --git a/app/(pages)/markets/page.tsx b/app/(pages)/markets/page.tsx new file mode 100644 index 0000000..aedd5cb --- /dev/null +++ b/app/(pages)/markets/page.tsx @@ -0,0 +1,147 @@ +"use client" + +import { useState, useEffect } from "react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table" +import { ArrowDown, ArrowUp } from "lucide-react" +import { getCrypto } from "@/app/utils/ServerProps" +import Image from "next/image" +import { useRouter } from "next/navigation" + +interface CryptoData { + symbol: string + current_price: string + priceChangePercent: string + volume: string + marketCap: string + image: string + name: string +} + +export default function CryptoList() { + const [cryptoData, setCryptoData] = useState([]) + const [sortColumn, setSortColumn] = useState("marketCap") + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc") + const [activeTab, setActiveTab] = useState("all") + const router = useRouter() + + const fetchData = async () => { + try { + const data = await getCrypto() + const formattedData: CryptoData[] = data.map((item: any) => ({ + image: item.image, + symbol: item.symbol, + current_price: item.current_price, + name: item.name, + priceChangePercent: parseFloat(item.price_change_percentage_24h).toFixed(2), + volume: formatVolume(parseFloat(item.total_volume)), + marketCap: formatMarketCap(parseFloat(item.market_cap)), + })) + setCryptoData(formattedData) + } catch (error) { + console.error("Error fetching data:", error) + } + } + + const formatVolume = (volume: number): string => { + if (volume >= 1e12) return `${(volume / 1e12).toFixed(2)}T` + if (volume >= 1e9) return `${(volume / 1e9).toFixed(2)}B` + if (volume >= 1e6) return `${(volume / 1e6).toFixed(2)}M` + return volume.toFixed(2) + } + + const formatMarketCap = (marketCap: number): string => { + if (marketCap >= 1e12) return `$${(marketCap / 1e12).toFixed(2)}T` + if (marketCap >= 1e9) return `$${(marketCap / 1e9).toFixed(2)}B` + if (marketCap >= 1e6) return `$${(marketCap / 1e6).toFixed(2)}M` + return `$${marketCap.toFixed(2)}` + } + + useEffect(() => { + fetchData() + const interval = setInterval(fetchData, 2000) + return () => clearInterval(interval) + }, []) + + const sortData = (column: keyof CryptoData) => { + if (column === sortColumn) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc") + } else { + setSortColumn(column) + setSortDirection("desc") + } + } + + const handleTabClick = (tab: string) => { + setActiveTab(tab) + if (tab === "24h") { + setSortColumn("priceChangePercent") + } else if (tab === "volume") { + setSortColumn("volume") + } else if (tab === "marketCap") { + setSortColumn("marketCap") + } + setSortDirection("desc") + } + + const sortedData = [...cryptoData].sort((a, b) => { + const aValue = parseFloat(a[sortColumn].replace(/[^\d.-]/g, "")) + const bValue = parseFloat(b[sortColumn].replace(/[^\d.-]/g, "")) + return sortDirection === "asc" ? aValue - bValue : bValue - aValue + }) + + const renderTable = (data: CryptoData[]) => ( +
+ + + {data.map((crypto) => ( + router.push(`/trade/${crypto.symbol}usdt`)} + > + + + {crypto.name} + {crypto.symbol.toUpperCase()} + + ${crypto.current_price} + + {crypto.priceChangePercent}% + + {crypto.volume} + {crypto.marketCap} + + ))} + +
+
+ ) + + return ( +
+ + + All + 24h Change + Volume + Market Cap + +
+
+

{activeTab === "all" ? "All Cryptocurrencies" : `Sorted by ${activeTab}`}

+ +
+ {renderTable(sortedData)} +
+
+
+ ) +} \ No newline at end of file diff --git a/app/(pages)/trade/[market]/page.tsx b/app/(pages)/trade/[market]/page.tsx new file mode 100644 index 0000000..48e0c39 --- /dev/null +++ b/app/(pages)/trade/[market]/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useState, useEffect } from "react"; +import useWebSocket from "react-use-websocket"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Ask } from "@/components/depth/AskTable"; +import { Bid } from "@/components/depth/BidTable"; +import { OrderUI } from "@/components/OrderUI"; +import { useParams } from "next/navigation"; +import { MarketBar } from "@/components/MarketBar"; +import TradeViewChart from "@/components/TradeView"; +import { Skeleton } from "@/components/ui/skeleton"; +import { TradeViewChartSkeleton } from "@/components/Skeletons/TradingViewSkeleton"; +import { MarketBarSkeleton } from "@/components/Skeletons/MarketBarSkeleton"; +import { AskSkeleton } from "@/components/Skeletons/AskBidSkeleton"; + +type Order = [string, string]; +type OrderBookUpdate = { + e: string; + E: number; + s: string; + U: number; + u: number; + b: Order[]; + a: Order[]; +}; + +type OrderBookState = { + bids: Map; + asks: Map; +}; + +export default function Markets() { + const { market } = useParams(); + const [orderBook, setOrderBook] = useState({ + bids: new Map(), + asks: new Map(), + }); + const [isLoading, setIsLoading] = useState(true); + const { lastJsonMessage, readyState } = useWebSocket( + `wss://stream.binance.com:9443/ws/${market}@depth` + ); + + useEffect(() => { + if (lastJsonMessage) { + const update = lastJsonMessage as OrderBookUpdate; + setIsLoading(false); + setOrderBook((prevOrderBook) => { + const newBids = new Map(prevOrderBook.bids); + const newAsks = new Map(prevOrderBook.asks); + + update.b.forEach(([price, quantity]) => { + if (parseFloat(quantity) === 0) { + newBids.delete(price); + } else { + newBids.set(price, quantity); + } + }); + + update.a.forEach(([price, quantity]) => { + if (parseFloat(quantity) === 0) { + newAsks.delete(price); + } else { + newAsks.set(price, quantity); + } + }); + + return { bids: newBids, asks: newAsks }; + }); + setIsLoading(false); + } + }, [lastJsonMessage]); + + const LoadingSkeleton = () => ( +
+ + +
+ ); + + return ( +
+ +
+ +
+ {isLoading ? ( + + ) : ( + + )} +
+
+ +
+ {isLoading ? : } + {isLoading ? : } +
+ +
+ {isLoading ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 03ae453..eed721d 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,76 +1,6 @@ -import NextAuth, { NextAuthOptions } from "next-auth" -import GoogleProvider from "next-auth/providers/google" -import CredentialsProvider from "next-auth/providers/credentials" -import { PrismaAdapter } from "@next-auth/prisma-adapter" -import { PrismaClient } from "@prisma/client" -import bcrypt from "bcryptjs" +import { authOptions } from "@/lib/auth" +import NextAuth from "next-auth" -const prisma = new PrismaClient() - -export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(prisma), - providers: [ - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), - CredentialsProvider({ - name: 'Credentials', - credentials: { - email: { label: "Email", type: "text" }, - password: { label: "Password", type: "password" } - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - return null - } - - const user = await prisma.user.findUnique({ - where: { email: credentials.email } - }) - //@ts-ignore - if (!user || !user.password) { - return null - } - //@ts-ignore - const isPasswordValid = await bcrypt.compare(credentials.password, user.password) - - if (!isPasswordValid) { - return null - } - - return { - id: user.id, - email: user.email, - name: user.name, - } - } - }) - ], - session: { - strategy: 'jwt' - }, - pages: { - signIn: '/auth/signin', - // signUp :"/api/signup" - }, - callbacks: { - async jwt({ token, user }) { - if (user) { - token.id = user.id - } - return token - }, - async session({ session, token }) { - if (session.user) { - //@ts-ignore - session.user.id = token.id as string - } - return session - }, - }, -} - -const handler = NextAuth(authOptions) +const handler = NextAuth(authOptions) export { handler as GET, handler as POST } \ No newline at end of file diff --git a/app/auth/error/page.tsx b/app/auth/error/page.tsx deleted file mode 100644 index ae35cbd..0000000 --- a/app/auth/error/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Error(){ - return( -
- -
- ) -} \ No newline at end of file diff --git a/app/auth/signIn/page.tsx b/app/auth/signIn/page.tsx deleted file mode 100644 index 4e380fc..0000000 --- a/app/auth/signIn/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import SignInComponent from "@/components/SiginBase"; - -export default function SignIn(){ - return( -
- -
- ) -} \ No newline at end of file diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx deleted file mode 100644 index 08bfb71..0000000 --- a/app/auth/signup/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { signIn } from 'next-auth/react' -import { useRouter } from 'next/navigation' -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card" - -export default function SignUp() { - const [error, setError] = useState(null) - const router = useRouter() - const { register, handleSubmit, formState: { errors } } = useForm() - - const onSubmit = async (data: any) => { - const response = await fetch('/api/auth/signup', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - - const result = await response.json() - - if (response.ok) { - const signInResult = await signIn('credentials', { - email: data.email, - password: data.password, - redirect: false, - }) - - if (signInResult?.error) { - setError(signInResult.error) - } else { - router.push('/') - } - } else { - setError(result.message) - } - } - - return ( - - - Sign Up - Create a new account - - -
-
-
- - - {errors.name && {errors.name.message as string}} -
-
- - - {errors.email && {errors.email.message as string}} -
-
- - - {errors.password && {errors.password.message as string}} -
-
- {error &&

{error}

} - -
-
- -

- Already have an account? Sign In -

-
-
- ) -} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 2b315c9..3fc9c4d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,6 @@ 'use client' import { SessionProvider } from 'next-auth/react' import './globals.css' -import { Appbar } from '@/components/Appbar' import { ThemeProvider } from './themeProvider' import NextTopLoader from 'nextjs-toploader'; export default function RootLayout({ @@ -9,6 +8,7 @@ export default function RootLayout({ }: { children: React.ReactNode }) { + return ( @@ -19,7 +19,6 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - {children} diff --git a/app/markets/page.tsx b/app/markets/page.tsx deleted file mode 100644 index 904c0d1..0000000 --- a/app/markets/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ - -export default function Page() { - return
-
- Markets page -
-
-} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index bdd8ba5..bf35f48 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,141 +1,106 @@ -"use client" +'use client' -import { useEffect, useState, createContext, useContext } from "react" +import { Button } from "@/components/ui/button" +import { ArrowRight, Bitcoin, DollarSign, LineChart, Lock, Zap } from "lucide-react" +import { signIn } from "next-auth/react" +import Link from "next/link" import { useRouter } from "next/navigation" -import Image from "next/image" -import { getCrypto } from "@/app/utils/ServerProps" -import { formatNumber } from "@/app/utils/Algorithms" -import useOnlineStatus from "@/hooks/onlineChecker" -import { Card, CardContent } from "@/components/ui/card" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Skeleton } from "@/components/ui/skeleton" -import { ArrowUpDown } from "lucide-react" - -type CryptoData = { - id: string - symbol: string - name: string - image: string - current_price: number - market_cap: number - market_cap_change_24h: number - price_change_percentage_24h: number -} - -const CryptoContext = createContext<{ cryptoData: CryptoData[], setCryptoData: React.Dispatch> } | null>(null) - -export default function CryptoDashboard() { - const isOnline = useOnlineStatus() - const [cryptoData, setCryptoData] = useState([]) - const [loading, setLoading] = useState(true) - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') - const router = useRouter() - - useEffect(() => { - const fetchAndUpdateCryptoData = async () => { - if (!isOnline) return - const data = await getCrypto() - setCryptoData(data) - setLoading(false) - } - - fetchAndUpdateCryptoData() - const interval = setInterval(fetchAndUpdateCryptoData, 10000) - return () => clearInterval(interval) - }, [isOnline]) - - const handleRedirect = (symbol: string, imageUrl: string) => { - localStorage.setItem("imageUrl", imageUrl) - router.push(`/trade/${symbol.toUpperCase()}_USDC`) - } - - const handleSort = () => { - const newOrder = sortOrder === 'asc' ? 'desc' : 'asc' - setSortOrder(newOrder) - const sortedData = [...cryptoData].sort((a, b) => { - return newOrder === 'asc' - ? a.price_change_percentage_24h - b.price_change_percentage_24h - : b.price_change_percentage_24h - a.price_change_percentage_24h - }) - setCryptoData(sortedData) - } - - if (!isOnline) { - return ( -
- - - Sorry, you are not connected to the internet. - - -
- ) - } +export default function CryptoLanding() { + const router = useRouter(); return ( - -
- - - {loading ? ( -
- {[...Array(10)].map((_, i) => ( -
- -
- - -
-
- ))} +
+
+ + + WebCrypto.ai + + +
+
+
+
+
+
+

+ Welcome to WebCrypto.ai +

+

+ Your gateway to learn cryptocurre ncies. Trade, invest, and grow your digital assets with help of ai. +

+
+
+ + +
+
+
+
+
+
+

Our Features

+
+
+ +

Real-time Trading

+

Execute trades instantly with our advanced trading engine.

+
+
+ +

Secure Storage

+

Your assets are protected with state-of-the-art security measures.

+
+
+ +

Lightning Fast

+

Experience rapid transactions and minimal latency.

+
+
+
+
+
+
+
+
+

Start Your Crypto Journey

+

+ Join thousands of traders and investors who trust CryptoHub for their cryptocurrency needs. Get started in minutes. +

- ) : ( -
- - - - Name - Price - Market Cap - - 24h Change - - - - - - {cryptoData.map(crypto => ( - handleRedirect(crypto.symbol, crypto.image)} - className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200" - > - -
- - {crypto.name} -
-
- ${crypto.current_price.toLocaleString()} - ${formatNumber(crypto.market_cap)} - - - {crypto.price_change_percentage_24h > 0 ? '+' : ''} - {crypto.price_change_percentage_24h.toFixed(2)}% - - -
- ))} -
-
+
+
- )} - - +
+
+
- +
+

© 2024 WebCrypto.ai. All rights reserved.

+ +
+
) } \ No newline at end of file diff --git a/app/test/page.tsx b/app/test/page.tsx index 8b6aab1..13d34aa 100644 --- a/app/test/page.tsx +++ b/app/test/page.tsx @@ -1,142 +1,10 @@ +import OrderDetails from '@/components/Wallet' +import React from 'react' -"use client" - -import { useEffect, useState, createContext, useContext } from "react" -import { useRouter } from "next/navigation" -import Image from "next/image" -import { getCrypto } from "@/app/utils/ServerProps" -import { formatNumber } from "@/app/utils/Algorithms" -import useOnlineStatus from "@/hooks/onlineChecker" -import { Card, CardContent } from "@/components/ui/card" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Skeleton } from "@/components/ui/skeleton" -import { ArrowUpDown } from "lucide-react" - -type CryptoData = { - id: string - symbol: string - name: string - image: string - current_price: number - market_cap: number - market_cap_change_24h: number - price_change_percentage_24h: number -} - -const CryptoContext = createContext<{ cryptoData: CryptoData[], setCryptoData: React.Dispatch> } | null>(null) - -export default function CryptoDashboard() { - const isOnline = useOnlineStatus() - const [cryptoData, setCryptoData] = useState([]) - const [loading, setLoading] = useState(true) - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') - const router = useRouter() - - useEffect(() => { - const fetchAndUpdateCryptoData = async () => { - if (!isOnline) return - const data = await getCrypto() - setCryptoData(data) - setLoading(false) - } - - fetchAndUpdateCryptoData() - const interval = setInterval(fetchAndUpdateCryptoData, 10000) - return () => clearInterval(interval) - }, [isOnline]) - - const handleRedirect = (symbol: string, imageUrl: string) => { - localStorage.setItem("imageUrl", imageUrl) - router.push(`/trade/${symbol.toUpperCase()}_USDC`) - } - - const handleSort = () => { - const newOrder = sortOrder === 'asc' ? 'desc' : 'asc' - setSortOrder(newOrder) - const sortedData = [...cryptoData].sort((a, b) => { - return newOrder === 'asc' - ? a.price_change_percentage_24h - b.price_change_percentage_24h - : b.price_change_percentage_24h - a.price_change_percentage_24h - }) - setCryptoData(sortedData) - } - - if (!isOnline) { - return ( -
- - - Sorry, you are not connected to the internet. - - -
- ) - } - +export default function OrderPage() { return ( - -
- - - {loading ? ( -
- {[...Array(10)].map((_, i) => ( -
- -
- - -
-
- ))} -
- ) : ( -
- - - - Name - Price - Market Cap - - 24h Change - - - - - - {cryptoData.map(crypto => ( - handleRedirect(crypto.symbol, crypto.image)} - className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200" - > - -
- - {crypto.name} -
-
- ${crypto.current_price.toLocaleString()} - ${formatNumber(crypto.market_cap)} - - - {crypto.price_change_percentage_24h > 0 ? '+' : ''} - {crypto.price_change_percentage_24h.toFixed(2)}% - - -
- ))} -
-
-
- )} -
-
-
-
+
+ +
) -} \ No newline at end of file +} diff --git a/app/trade/[market]/page.tsx b/app/trade/[market]/page.tsx deleted file mode 100644 index 0d5e34c..0000000 --- a/app/trade/[market]/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { Depth } from "@/components/depth/Depth"; -import { MarketBar } from "@/components/MarketBar"; -import { OrderUI } from "@/components/OrderUI"; -import { TradeView } from "@/components/TradeView"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; - -export default function Page() { - const { market } = useParams(); - const [isMobile, setIsMobile] = useState(false); - - useEffect(() => { - const checkIfMobile = () => { - setIsMobile(window.innerWidth < 768); - }; - - checkIfMobile(); - window.addEventListener('resize', checkIfMobile); - - return () => window.removeEventListener('resize', checkIfMobile); - }, []); - - return ( -
-
- -
-
- -
- {!isMobile && ( -
- -
- )} -
-
- {!isMobile &&
} -
- -
-
- ); -} \ No newline at end of file diff --git a/app/utils/Chart.ts b/app/utils/Chart.ts new file mode 100644 index 0000000..ffcf50e --- /dev/null +++ b/app/utils/Chart.ts @@ -0,0 +1,168 @@ +// Lightweight Charts™ Example: Realtime updates +// https://tradingview.github.io/lightweight-charts/tutorials/demos/realtime-updates + +import { createChart } from "lightweight-charts"; + +let randomFactor = 25 + Math.random() * 25; +const samplePoint = i => + i * + (0.5 + + Math.sin(i / 1) * 0.2 + + Math.sin(i / 2) * 0.4 + + Math.sin(i / randomFactor) * 0.8 + + Math.sin(i / 50) * 0.5) + + 200 + + i * 2; + +function generateData( + numberOfCandles = 500, + updatesPerCandle = 5, + startAt = 100 +) { + const createCandle = (val, time) => ({ + time, + open: val, + high: val, + low: val, + close: val, + }); + + const updateCandle = (candle, val) => ({ + time: candle.time, + close: val, + open: candle.open, + low: Math.min(candle.low, val), + high: Math.max(candle.high, val), + }); + + randomFactor = 25 + Math.random() * 25; + const date = new Date(Date.UTC(2018, 0, 1, 12, 0, 0, 0)); + const numberOfPoints = numberOfCandles * updatesPerCandle; + const initialData = []; + const realtimeUpdates = []; + let lastCandle; + let previousValue = samplePoint(-1); + for (let i = 0; i < numberOfPoints; ++i) { + if (i % updatesPerCandle === 0) { + date.setUTCDate(date.getUTCDate() + 1); + } + const time = date.getTime() / 1000; + let value = samplePoint(i); + const diff = (value - previousValue) * Math.random(); + value = previousValue + diff; + previousValue = value; + if (i % updatesPerCandle === 0) { + const candle = createCandle(value, time); + lastCandle = candle; + if (i >= startAt) { + realtimeUpdates.push(candle); + } + } else { + const newCandle = updateCandle(lastCandle, value); + lastCandle = newCandle; + if (i >= startAt) { + realtimeUpdates.push(newCandle); + } else if ((i + 1) % updatesPerCandle === 0) { + initialData.push(newCandle); + } + } + } + + return { + initialData, + realtimeUpdates, + }; +} + +const chartOptions = { + layout: { + textColor: 'white', + background: { type: 'solid', color: 'black' }, + }, + height: 200, +}; +const container = document.getElementById('container'); +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(container, chartOptions); + +// Only needed within demo page +// eslint-disable-next-line no-undef +window.addEventListener('resize', () => { + chart.applyOptions({ height: 200 }); +}); + +const series = chart.addCandlestickSeries({ + upColor: '#26a69a', + downColor: '#ef5350', + borderVisible: false, + wickUpColor: '#26a69a', + wickDownColor: '#ef5350', +}); + +const data = generateData(2500, 20, 1000); + +series.setData(data.initialData); +chart.timeScale().fitContent(); +chart.timeScale().scrollToPosition(5); + +// simulate real-time data +function* getNextRealtimeUpdate(realtimeData) { + for (const dataPoint of realtimeData) { + yield dataPoint; + } + return null; +} +const streamingDataProvider = getNextRealtimeUpdate(data.realtimeUpdates); + +const intervalID = setInterval(() => { + const update = streamingDataProvider.next(); + if (update.done) { + clearInterval(intervalID); + return; + } + series.update(update.value); +}, 100); + +const styles = ` + .buttons-container { + display: flex; + flex-direction: row; + gap: 8px; + } + .buttons-container button { + all: initial; + font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, + sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 510; + line-height: 24px; /* 150% */ + letter-spacing: -0.32px; + padding: 8px 24px; + color: rgba(19, 23, 34, 1); + background-color: rgba(240, 243, 250, 1); + border-radius: 8px; + cursor: pointer; + } + + .buttons-container button:hover { + background-color: rgba(224, 227, 235, 1); + } + + .buttons-container button:active { + background-color: rgba(209, 212, 220, 1); + } +`; + +const stylesElement = document.createElement('style'); +stylesElement.innerHTML = styles; +container.appendChild(stylesElement); + +const buttonsContainer = document.createElement('div'); +buttonsContainer.classList.add('buttons-container'); +const button = document.createElement('button'); +button.innerText = 'Go to realtime'; +button.addEventListener('click', () => chart.timeScale().scrollToRealTime()); +buttonsContainer.appendChild(button); + +container.appendChild(buttonsContainer); \ No newline at end of file diff --git a/app/utils/ServerProps.ts b/app/utils/ServerProps.ts index 0089037..7102d4a 100644 --- a/app/utils/ServerProps.ts +++ b/app/utils/ServerProps.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { Depth, KLine, Trade } from "./types"; -const BASE_URL = "https://exchange-proxy.100xdevs.com/api/v1"; +const BASE_URL = "https://api.backpack.exchange/api/v1"; const Market_URL = "https://price-indexer.workers.madlads.com/?ids=solana,usd-coin,pyth-network,jito-governance-token,tether,bonk,helium,helium-mobile,bitcoin,ethereum,dogwifcoin,jupiter-exchange-solana,parcl,render-token,sharky-fi,tensor,wormhole,wen-4,cat-in-a-dogs-world,book-of-meme,raydium,hivemapper,kamino,drift-protocol,nyan,jeo-boden,habibi-sol,io,zeta,mother-iggy,shuffle-2,pepe,shiba-inu,chainlink,uniswap,ondo-finance,holograph,starknet,matic-network,fantom,mon-protocol,blur,worldcoin-wld,polyhedra-network,unagi-token,layerzero" export async function getTicker(market: string): Promise { @@ -14,8 +14,13 @@ export async function getTicker(market: string): Promise { } export async function getTickers(): Promise { - const response = await axios.get(`${BASE_URL}/tickers`); - return response.data; + try { + const response = await axios.get(`${BASE_URL}/tickers`); + return response.data; + } catch (error) { + console.error("THIS IS THE ERROR : ", error); + return []; + } } diff --git a/app/utils/types.ts b/app/utils/types.ts index 2530acf..440e7e2 100644 --- a/app/utils/types.ts +++ b/app/utils/types.ts @@ -45,11 +45,6 @@ export const orderVefication = zod.object({ name : zod.string(), }) -export const loginVerfication = zod.object({ - username : zod.string().min(6, "Username Atleast 6 characters"), - password: zod.string().min(8,"Password Atleast 8 characters"), -}) - export interface SellProps { crypto:string amount : string @@ -61,3 +56,30 @@ export interface AuthProps{ username:string, password:string } + + +export interface TickerProps { + symbol: string; + lastPrice: string; + high: string; + low: string; + volume: string; + quoteVolume: string; + priceChange: string; + priceChangePercent: string; +} + +export interface DepthProps { + bids: [string, string][]; + asks: [string, string][]; +} + +export interface KlineProps { + openTime: number; + open: string; + high: string; + low: string; + close: string; + volume: string; + closeTime: number; +} \ No newline at end of file diff --git a/components/Appbar.tsx b/components/Appbar.tsx index 32ebb6e..959be8c 100644 --- a/components/Appbar.tsx +++ b/components/Appbar.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { PrimaryButton, SuccessButton } from "./core/Button" -import { Menu, X } from 'lucide-react'; +import { PrimaryButton, SuccessButton } from "./core/Button"; +import { Menu, Wallet, X } from "lucide-react"; import UserDetails from "./UserDetails"; import DarkModeToggle from "./DarkModeToggle"; import { signIn, useSession } from "next-auth/react"; @@ -11,77 +11,95 @@ import { Input } from "./ui/input"; import { Button } from "./ui/button"; export const Appbar = () => { - const route = usePathname(); - const router = useRouter(); - const [menuOpen, setMenuOpen] = useState(false); - const session = useSession(); + const route = usePathname(); + const router = useRouter(); + const [menuOpen, setMenuOpen] = useState(false); + const session = useSession(); - const handleDeposit = () => { - if (session.data?.user) { - router.push("/deposit") - } else { - console.log("Please Signin") - } + const handleDeposit = () => { + if (session.data?.user) { + router.push("/deposit"); + } else { + console.log("Please Signin"); } + }; - const NavItem = ({ href, children }:any) => ( -
{ router.push(href); setMenuOpen(false); }} - > - {children} -
- ); + const NavItem = ({ href, children }: any) => ( +
{ + router.push(href); + setMenuOpen(false); + }} + > + {children} +
+ ); - return ( -
-
-
-
router.push('/')}> - Learn Web3 -
-
- Markets - Trade -
-
-
- {session.data?.user ? ( -
- -
- ) : ( - - )} - -
-
- - - - {session.data?.user ? ( -
- router.push(":id/wallet")}>Wallet - -
- ) : ( - - )} -
+ return ( +
+
+
+
router.push("/")} + > + WebCrypto.ai +
+
+ Markets + Trade +
+
+
+ {session.data?.user ? ( +
+
- {menuOpen && ( -
- Markets - Trade - - Deposit - {session.data?.user && ( - router.push(":id/wallet")} className="mt-2">Wallet - )} -
- )} + ) : ( + + )} + +
+
+ + + + {session.data?.user ? ( +
+ router.push(":id/wallet")}> + Wallet + + +
+ ) : ( + + )} +
+
+ {menuOpen && ( +
+ Markets + Trade + + + Deposit + + {session.data?.user && ( + + )}
- ); -} \ No newline at end of file + )} +
+ ); +}; diff --git a/components/CryptoTable.tsx b/components/CryptoTable.tsx new file mode 100644 index 0000000..7279b4a --- /dev/null +++ b/components/CryptoTable.tsx @@ -0,0 +1,52 @@ +import { TableBody, TableCell, TableRow, Table } from "@/components/ui/table" +import Image from "next/image" +import { useRouter } from "next/navigation" + +interface CryptoData { + symbol: string + current_price: string + priceChangePercent: string + volume: string + marketCap: string + image: string + name: string +} + +interface CryptoTableProps { + data: CryptoData[] +} + +export function CryptoTable({ data }: CryptoTableProps) { + const router = useRouter() + + return ( +
+ + + {data.map((crypto) => ( + router.push(`/trade/${crypto.symbol}usdt`)} + > + + + {crypto.name} + {crypto.symbol.toUpperCase()} + + ${crypto.current_price} + + {crypto.priceChangePercent}% + + {crypto.marketCap} + + ))} + +
+
+ ) +} \ No newline at end of file diff --git a/components/MarketBar.tsx b/components/MarketBar.tsx index 1bbbcce..f8e3ccf 100644 --- a/components/MarketBar.tsx +++ b/components/MarketBar.tsx @@ -1,82 +1,56 @@ -"use client"; -import { useEffect, useState } from "react"; -import Image from "next/image"; -import { TickerProps } from "@/app/utils/types"; -import { getTicker } from "@/app/utils/ServerProps"; -import { SignalingManager } from "@/app/utils/SignalingManager"; +"use client" -export const MarketBar = ({market}: {market: string}) => { - const [ticker, setTicker] = useState(null); - useEffect(() => { - getTicker(market).then(setTicker); - SignalingManager.getInstance().registerCallback("ticker", (data: Partial) => setTicker(prevTicker => ({ - firstPrice: data?.firstPrice ?? prevTicker?.firstPrice ?? '', - high: data?.high ?? prevTicker?.high ?? '', - lastPrice: data?.lastPrice ?? prevTicker?.lastPrice ?? '', - low: data?.low ?? prevTicker?.low ?? '', - priceChange: data?.priceChange ?? prevTicker?.priceChange ?? '', - priceChangePercent: data?.priceChangePercent ?? prevTicker?.priceChangePercent ?? '', - quoteVolume: data?.quoteVolume ?? prevTicker?.quoteVolume ?? '', - symbol: data?.symbol ?? prevTicker?.symbol ?? '', - trades: data?.trades ?? prevTicker?.trades ?? '', - volume: data?.volume ?? prevTicker?.volume ?? '', - })), `TICKER-${market}`); - SignalingManager.getInstance().sendMessage({"method":"SUBSCRIBE","params":[`ticker.${market}`]} ); - return () => { - SignalingManager.getInstance().deRegisterCallback("ticker", `TICKER-${market}`); - SignalingManager.getInstance().sendMessage({"method":"UNSUBSCRIBE","params":[`ticker.${market}`]} ); - } - }, [market]) - // +import { Card, CardContent } from "@/components/ui/card" +import { ArrowUpIcon, ArrowDownIcon } from 'lucide-react' +import UseMarketWebsockets from "@/hooks/MarketWebsockets" +import { MarketBarSkeleton } from "./Skeletons/MarketBarSkeleton" - return
-
-
- -
-
-

${ticker?.lastPrice}

-

${ticker?.lastPrice}

-
-
-

24H Change

-

0 ? "text-green-500" : "text-red-500"}`}>{Number(ticker?.priceChange) > 0 ? "+" : ""} {ticker?.priceChange} {Number(ticker?.priceChangePercent)?.toFixed(2)}%

-

24H High

-

{ticker?.high}

-
-
-

24H Low

-

{ticker?.low}

-
- -
-
-
-
+export function MarketBar({ market}: { market : string }) { + const { marketData, priceChangeColor } = UseMarketWebsockets(market) -} + if (!marketData) { + return + } -function Ticker({market}: {market: string}) { - const image = localStorage.getItem("imageUrl") || "" - return
-
- {"nothing"} - USDC Logo -
- -
-} \ No newline at end of file + + + ) +} diff --git a/components/OrderUI.tsx b/components/OrderUI.tsx index 5c91517..b0adbb4 100644 --- a/components/OrderUI.tsx +++ b/components/OrderUI.tsx @@ -1,153 +1,182 @@ -"use client"; -import Image from "next/image"; -import { useEffect, useState } from "react"; -import { BuyCrypto } from "@/app/(server)/api/BuyCrypto"; +"use client" -export function OrderUI({ market}: {market: string }) { - const [amount, setAmount] = useState(""); - const [activeTab, setActiveTab] = useState('buy'); - const [type, setType] = useState('limit'); - const [buy , setBuy] = useState(true); - const [sell , setSell] = useState(false) - const [image, setImage] = useState(""); - const hanldeBuyCrypto = async () => { - const order = await BuyCrypto( market, amount ) - console.log(order) - } - useEffect(() => { - if (typeof window !== "undefined") { - const image = window.localStorage.getItem("imageUrl"); - //@ts-ignore - setImage(image) - } - }, []); - - return
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-

Available Balance

-

13442.94 USDC

-
-
-
-

- Price -

-
- setAmount(e.target.value)} /> -
-
- -
-
-
-
-
-
-

- Quantity -

-
- setAmount(e.target.value)} /> -
-
- -
-
-
-
-
-

≈ 0.00 USDC

-
-
-
- 25% -
-
- 50% -
-
- 75% -
-
- Max -
-
-
- -
-
-
- - -
-
- - -
-
-
-
-
-
-
-
-} +import { useState, useEffect, useRef } from "react" +import Image from "next/image" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" +import { Card, CardContent } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" -function LimitButton({ type, setType }: { type: string, setType: any }) { - return
setType('limit')}> -
- Limit -
-
+async function BuyCrypto(market: string, amount: string) { + // Placeholder for API call + console.log(`Buying ${amount} of ${market}`) + return { success: true } } -function MarketButton({ type, setType }: { type: string, setType: any }) { - return
setType('market')}> -
- Market -
-
-} +export function OrderUI({ market }: { market: string }) { + const [price, setPrice] = useState("") + const [quantity, setQuantity] = useState("") + const [activeTab, setActiveTab] = useState("buy") + const [orderType, setOrderType] = useState("limit") + const [image, setImage] = useState("") + const [marketPrice, setMarketPrice] = useState(0) + const ws = useRef(null) -function BuyButton({ activeTab, setActiveTab }: { activeTab: string, setActiveTab: any }) { - return
setActiveTab('buy')}> -

- Buy -

-
-} + useEffect(() => { + if (typeof window !== "undefined") { + const storedImage = localStorage.getItem("imageUrl") + if (storedImage) setImage(storedImage) + } + + // Initialize WebSocket connection + ws.current = new WebSocket(`wss://stream.binance.com:9443/ws/${market.toLowerCase()}@ticker`) + + ws.current.onmessage = (event) => { + const data = JSON.parse(event.data) + setMarketPrice(parseFloat(data.c)) + } + + return () => { + if (ws.current) { + ws.current.close() + } + } + }, [market]) + + const handleOrder = async () => { + if (activeTab === "buy") { + const order = await BuyCrypto(market, quantity) + console.log(order) + } else { + console.log(`Selling ${quantity} of ${market}`) + } + } -function SellButton({ activeTab, setActiveTab }: { activeTab: string, setActiveTab: any }) { - return
setActiveTab('sell')}> -

- Sell -

-
+ const handlePriceChange = (e: React.ChangeEvent) => { + const newPrice = e.target.value + setPrice(newPrice) + if (newPrice && quantity) { + const newQuantity = (parseFloat(quantity) * marketPrice / parseFloat(newPrice)).toFixed(8) + setQuantity(newQuantity) + } + } + + const handleQuantityChange = (e: React.ChangeEvent) => { + const newQuantity = e.target.value + setQuantity(newQuantity) + if (newQuantity && price) { + const newPrice = (parseFloat(price) * parseFloat(newQuantity) / parseFloat(quantity)).toFixed(8) + setPrice(newPrice) + } + } + + const calculateFee = () => { + const total = parseFloat(price) * parseFloat(quantity) + return isNaN(total) ? "0.00000000" : (total * 0.002).toFixed(8) // 0.2% fee + } + + const calculateTotal = () => { + const total = parseFloat(price) * parseFloat(quantity) + return isNaN(total) ? "0.00" : total.toFixed(2) + } + + return ( + + + + Buy + Sell + + +
+
+ Available Balance + 13442.94 USDC +
+
+ +
+ + +
+
+
+ +
+ +
+ USDC +
+
+
+
+ +
+ +
+ {image && Crypto} +
+
+
+
+ ≈ {calculateTotal()} USDC + Fee: {calculateFee()} USDC +
+
+ {["25%", "50%", "75%", "Max"].map((percent) => ( + + ))} +
+ +
+
+ + +
+
+ + +
+
+
+ Market Price: {marketPrice.toFixed(8)} USDC +
+
+
+
+
+ ) } \ No newline at end of file diff --git a/components/SiginBase.tsx b/components/SiginBase.tsx deleted file mode 100644 index 2a7a9b7..0000000 --- a/components/SiginBase.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client' -import { Button } from "@/components/ui/button" -import { signIn, useSession } from "next-auth/react" -import { useState , useEffect } from "react" -import { motion, AnimatePresence } from 'framer-motion' -import { useRouter } from "next/navigation" - -export default function SignInComponent() { - const [showContent, setShowContent] = useState(false) - const router = useRouter(); - const session = useSession(); - const handleGoogle = async () => { - await signIn('google') - router.push("/") - } - - useEffect(()=>{ - setShowContent(true) - }, []) - - - - if(session?.data?.user) { - router.push("/") - return( -
Already Logged , Please Wait ....
- ) - } - - const containerVariants = { - hidden: { opacity: 0, y: -20 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.6, - ease: "easeOut" - } - } - } - - const buttonVariants = { - hidden: { opacity: 0, scale: 0.8 }, - visible: { - opacity: 1, - scale: 1, - transition: { - delay: 0.3, - type: "spring", - stiffness: 200, - damping: 15 - } - }, - hover: { - scale: 1.05, - boxShadow: "0px 0px 8px rgba(66, 133, 244, 0.3)", - transition: { duration: 0.2 } - }, - tap: { scale: 0.95 } - } - - return ( -
- - {showContent && ( - - - Welcome - - - - - - - - By signing in, you agree to our Terms of Service and Privacy Policy - - - )} - -
- ) -} \ No newline at end of file diff --git a/components/SignInForm.tsx b/components/SignInForm.tsx new file mode 100644 index 0000000..cf076ba --- /dev/null +++ b/components/SignInForm.tsx @@ -0,0 +1,55 @@ +'use client' + +import { useForm } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import * as yup from 'yup' +import { signIn } from 'next-auth/react' +import { useState } from 'react' +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { SignInSchema } from '@/lib/types' + + +type SignInFormData = yup.InferType + +export default function SignInForm() { + const [error, setError] = useState(null) + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: yupResolver(SignInSchema) + }) + + const onSubmit = async (data: SignInFormData) => { + const result = await signIn('credentials', { + redirect: false, + email: data.email, + password: data.password, + }) + + if (result?.error) { + setError(result.error) + } + } + + return ( +
+
+ + + {errors.email &&

{errors.email.message}

} +
+
+ + + {errors.password &&

{errors.password.message}

} +
+ {error && ( + + {error} + + )} + +
+ ) +} \ No newline at end of file diff --git a/components/SignUpForm.tsx b/components/SignUpForm.tsx new file mode 100644 index 0000000..ccd8723 --- /dev/null +++ b/components/SignUpForm.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useForm } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import * as yup from 'yup' +import { useState } from 'react' +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { SignUpSchema } from '@/lib/types' + + +type SignUpFormData = yup.InferType + +export default function SignUpForm() { + const [error, setError] = useState(null) + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: yupResolver(SignUpSchema) + }) + + const onSubmit = async (data: SignUpFormData) => { + try { + const response = await fetch('/api/auth/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.message || 'Something went wrong') + } + + } catch (err) { + //@ts-ignore + setError(err.message) + } + } + + return ( +
+
+ + + {errors.name &&

{errors.name.message}

} +
+
+ + + {errors.email &&

{errors.email.message}

} +
+
+ + + {errors.password &&

{errors.password.message}

} +
+
+ + + {errors.confirmPassword &&

{errors.confirmPassword.message}

} +
+ {error && ( + + {error} + + )} + +
+ ) +} \ No newline at end of file diff --git a/components/SignupBase.tsx b/components/SignupBase.tsx deleted file mode 100644 index 8baa926..0000000 --- a/components/SignupBase.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// 'use client' -// import React from 'react' -// import Link from 'next/link'; -// import { useRouter } from 'next/navigation'; -// import { Input } from '@/components/ui/input'; -// import { Button } from '@/components/ui/button'; -// import { useToast } from '@/components/ui/use-toast'; - - - -// export default function SignUp() : React.ReactNode { -// const [error,setError] = React.useState(null); -// const [loading,setLoading] = React.useState(false); -// const [data,setData ] = React.useState({username: "" , password : ""}); -// const router = useRouter(); -// const { toast } = useToast() - -// const handleLogin = async()=>{ -// try { -// console.log(data) -// const safeData = loginVerfication.safeParse(data) -// if(safeData.success){ -// const response:any = await Login(safeData?.data?.username, safeData?.data?.password) -// toast({ -// variant:"default", -// title: response.response -// }) -// if(response.status === 200){ -// router.push("/") -// } -// } -// else if(!safeData.success) { -// console.log(safeData) -// setError("Please Enter Valid Data \n. Username Aleast 6 Letter And No Space Are Allowed. \n PassWord Atleast 8 Chacretors. \n") -// } -// } catch (error) { -// console.log(error) -// setError(error as string) -// } -// } - -// return ( -//
-// {error &&

{error}

} -// setData({...data , username: e.target.value})} -// /> -// setData({...data , password: e.target.value})}/> -// -//
-//

No Account ? -// SignIn -//

-//
-//
-// ) -// }; \ No newline at end of file diff --git a/components/Skeletons/AskBidSkeleton.tsx b/components/Skeletons/AskBidSkeleton.tsx new file mode 100644 index 0000000..37bc55e --- /dev/null +++ b/components/Skeletons/AskBidSkeleton.tsx @@ -0,0 +1,27 @@ +import { Table, TableCell, TableHead, TableHeader, TableRow, TableBody } from "@/components/ui/table" +import { Skeleton } from "@/components/ui/skeleton" + +export function AskSkeleton() { + return ( + + + + Price + Quantity + + + + {[...Array(10)].map((_, index) => ( + + + + + + + + + ))} + +
+ ) +} \ No newline at end of file diff --git a/components/Skeletons/MarketBarSkeleton.tsx b/components/Skeletons/MarketBarSkeleton.tsx new file mode 100644 index 0000000..09b9472 --- /dev/null +++ b/components/Skeletons/MarketBarSkeleton.tsx @@ -0,0 +1,28 @@ +import { Card, CardContent } from "../ui/card"; + +export function MarketBarSkeleton() { + return ( + + +
+
+
+
+
+
+
+
+
+
+ {[...Array(4)].map((_, index) => ( +
+
+
+
+ ))} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/Skeletons/TradingViewSkeleton.tsx b/components/Skeletons/TradingViewSkeleton.tsx new file mode 100644 index 0000000..f3ffcd5 --- /dev/null +++ b/components/Skeletons/TradingViewSkeleton.tsx @@ -0,0 +1,22 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Skeleton } from "@/components/ui/skeleton" + +export function TradeViewChartSkeleton() { + return ( +
+ + + + 5m + 30m + 1h + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/components/TradeView.tsx b/components/TradeView.tsx index 913e2c1..a320967 100644 --- a/components/TradeView.tsx +++ b/components/TradeView.tsx @@ -1,147 +1,200 @@ -'use client' - -import { ChartManager } from "@/app/utils/ChartManager"; -import { getKlines } from "@/app/utils/ServerProps"; -import { KLine } from "@/app/utils/types"; -import { useEffect, useRef, useState } from "react"; - -export function TradeView({ - market, -}: { +"use client"; + +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + createChart, + ColorType, + IChartApi, + ISeriesApi, + UTCTimestamp, + CandlestickData, +} from "lightweight-charts"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +type TimeframeKey = "5m" | "30m" | "1h"; + +const timeframes: Record = { + "5m": 5, + "30m": 30, + "1h": 60, +}; + +interface TradeViewChartProps { market: string; -}) { - const chartRef = useRef(null); - const chartManagerRef = useRef(null); - const [chartHeight, setChartHeight] = useState("520px"); - const [timeframe, setTimeframe] = useState("1h"); - - useEffect(() => { - const updateChartDimensions = () => { - if (window.innerWidth < 768) { - setChartHeight("300px"); - } else { - setChartHeight("520px"); - } - }; +} - updateChartDimensions(); - window.addEventListener('resize', updateChartDimensions); +export default function TradeViewChart({ market }: TradeViewChartProps) { + const chartContainerRef = useRef(null); + const chartRef = useRef(null); + const candlestickSeriesRef = useRef | null>(null); + const wsRef = useRef(null); + const [timeframe, setTimeframe] = useState("5m"); + const [coinId, setCoinId] = useState("bitcoin"); - return () => window.removeEventListener('resize', updateChartDimensions); + useEffect(() => { + if (chartContainerRef.current) { + const chart = createChart(chartContainerRef.current, { + width: chartContainerRef.current.clientWidth, + height: 400, + layout: { + background: { type: ColorType.Solid, color: "white" }, + textColor: "black", + }, + grid: { + vertLines: { color: "#e0e0e0" }, + horzLines: { color: "#e0e0e0" }, + }, + }); + + chartRef.current = chart; + + const candlestickSeries = chart.addCandlestickSeries(); + candlestickSeriesRef.current = candlestickSeries; + + const handleResize = () => { + chart.applyOptions({ width: chartContainerRef.current!.clientWidth }); + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + chart.remove(); + }; + } }, []); - const fetchKlineData = async (start: number, end: number, tf: string) => { - try { - return await getKlines(market, tf, start, end); - } catch (e) { - console.error("Failed to fetch kline data:", e); - throw new Error("Failed to fetch kline data"); - } - }; + const fetchHistoricalData = useCallback(async () => { + const days = 30; // Fetch 30 days of data + const url = `https://api.coingecko.com/api/v3/coins/${coinId}/ohlc?vs_currency=usd&days=${days}`; - const updateChart = async (start: number, end: number, tf: string) => { try { - const klineData = await fetchKlineData(start, end, tf); - if (chartManagerRef.current) { - chartManagerRef.current.updateData( - klineData.map((x: KLine) => ({ - close: parseFloat(x.close), - high: parseFloat(x.high), - low: parseFloat(x.low), - open: parseFloat(x.open), - timestamp: new Date(x.end), - })).sort((x, y) => (x.timestamp < y.timestamp ? -1 : 1)) - ); - } + const response = await fetch(url); + const data: number[][] = await response.json(); + const candlesticks: CandlestickData[] = data + .filter((d) => d[0] % (timeframes[timeframe] * 60 * 1000) === 0) + .map((d) => ({ + time: (d[0] / 1000) as UTCTimestamp, + open: d[1], + high: d[2], + low: d[3], + close: d[4], + })); + candlestickSeriesRef.current?.setData(candlesticks); } catch (error) { - console.error("Error updating chart:", error); + console.error("Error fetching historical data:", error); } - }; + }, [coinId, timeframe]); - useEffect(() => { - const init = async () => { - const end = Math.floor(new Date().getTime() / 1000); - const start = end - 60 * 60 * 24 * 7; // 7 days ago - let klineData: KLine[] = []; + const setupWebSocket = useCallback(() => { + if (wsRef.current) { + wsRef.current.close(); + } - try { - klineData = await fetchKlineData(start, end, timeframe); - } catch (error) { - console.error("Error fetching initial kline data:", error); - return; + const wsUrl = `wss://stream.binance.com:9443/ws/${coinId}usdt@kline_${timeframe}`; + const ws = new WebSocket(wsUrl); + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.k) { + const { t, o, h, l, c } = data.k; + const newCandle: CandlestickData = { + time: (t / 1000) as UTCTimestamp, + open: parseFloat(o), + high: parseFloat(h), + low: parseFloat(l), + close: parseFloat(c), + }; + candlestickSeriesRef.current?.update(newCandle); } + }; - if (chartRef.current) { - if (chartManagerRef.current) { - chartManagerRef.current.destroy(); - } + ws.onerror = (error) => { + console.error("WebSocket error:", error); + }; - const chartManager = new ChartManager( - chartRef.current, - klineData.map((x: KLine) => ({ - close: parseFloat(x.close), - high: parseFloat(x.high), - low: parseFloat(x.low), - open: parseFloat(x.open), - timestamp: new Date(x.end), - })).sort((x, y) => (x.timestamp < y.timestamp ? -1 : 1)), - { - background: "#0e0f14", - color: "white", - } - ); - - chartManagerRef.current = chartManager; - - // Add event listeners for chart movements and zoom changes - chartManager.on('update', () => { - const visibleRange = chartManager.getVisibleRange(); - if (visibleRange) { - const { from, to } = visibleRange; - const range = to.getTime() - from.getTime(); - let newTimeframe = timeframe; - - // Adjust timeframe based on visible range - if (range <= 1000 * 60 * 60) { // 1 hour or less - newTimeframe = '1m'; - } else if (range <= 1000 * 60 * 60 * 24) { // 1 day or less - newTimeframe = '15m'; - } else if (range <= 1000 * 60 * 60 * 24 * 7) { // 1 week or less - newTimeframe = '1h'; - } else { - newTimeframe = '1d'; - } - - if (newTimeframe !== timeframe) { - setTimeframe(newTimeframe); - updateChart(Math.floor(from.getTime() / 1000), Math.floor(to.getTime() / 1000), newTimeframe); - } - } - }); + wsRef.current = ws; + }, [coinId, timeframe]); - const handleResize = () => { - if (chartManagerRef.current) { - chartManagerRef.current.resize(); - } - }; + useEffect(() => { + fetchHistoricalData(); + setupWebSocket(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); + return () => { + if (wsRef.current) { + wsRef.current.close(); } }; + }, [fetchHistoricalData, setupWebSocket]); + + const fetchMoreHistoricalData = useCallback( + async (endTime: number) => { + const days = 30; + const url = `https://api.coingecko.com/api/v3/coins/${coinId}/ohlc?vs_currency=usd&days=${days}&end_date=${endTime}`; + + try { + const response = await fetch(url); + const data: number[][] = await response.json(); + const candlesticks: CandlestickData[] = data + .filter((d) => d[0] % (timeframes[timeframe] * 60 * 1000) === 0) + .map((d) => ({ + time: (d[0] / 1000) as UTCTimestamp, + open: d[1], + high: d[2], + low: d[3], + close: d[4], + })); + const currentData = + candlestickSeriesRef.current?.data() as CandlestickData[]; + candlestickSeriesRef.current?.setData([ + ...candlesticks, + ...currentData, + ]); + } catch (error) { + console.error("Error fetching more historical data:", error); + } + }, + [coinId, timeframe] + ); + + useEffect(() => { + if (chartRef.current) { + chartRef.current.timeScale().subscribeVisibleLogicalRangeChange(() => { + const logicalRange = chartRef.current + ?.timeScale() + .getVisibleLogicalRange(); + if (logicalRange && logicalRange.from < 10) { + const firstCandle = candlestickSeriesRef.current?.dataByIndex(0, 1); + if (firstCandle) { + const endTime = firstCandle.time as number; + fetchMoreHistoricalData(endTime); + } + } + }); + } + }, [timeframe, fetchMoreHistoricalData, coinId]); + + const handleTimeframeChange = (newTimeframe: TimeframeKey) => { + setTimeframe(newTimeframe); + }; - init(); - }, [market, timeframe]); - - return( -
- ) -} \ No newline at end of file + return ( +
+

Crypto Candlestick Chart

+ handleTimeframeChange(value as TimeframeKey)} + > + + 5m + 30m + 1h + + + + + +
+
+ ); +} diff --git a/components/Wallet.tsx b/components/Wallet.tsx index c9158a0..c2b0225 100644 --- a/components/Wallet.tsx +++ b/components/Wallet.tsx @@ -1,145 +1,178 @@ -import { PrismaClient } from "@prisma/client" +import { prisma } from "@/lib/prisma"; +import { getServerSession } from "next-auth"; -export default async function OrderDetails(){ - const prisma = new PrismaClient() - return( -
-
-
-
-

Order History

+export default async function OrderDetails() { + const session = await getServerSession(); + + if (!session) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const cryptoData = await prisma.user.findFirst({ + where: { + //@ts-ignore + email: session?.user?.email + }, + include:{ + crypto: true, + order:true + } + }); + console.log(cryptoData) + return ( +
+
+
+
+

Order History

+
+
+
+
+
+

Current Balance

+
+
+
+
+
+ + + + Bitcoin (BTC) +
+
+
0.25
+
+ $7,500 (at $30,000 per BTC) +
+
+
+
+
+ + + + + Ethereum +
+
+
1.5
+
+ $3,000 (at $2,000 per ETH) +
-
+
+
+
+ + + + + + + + + Dogeco
+
+
5,000
+
+ $500 (at $0.10 per DOGE) +
-
-
-

Current Balance

+
+
+
+ + + + Litecoin (LTC)
-
-
-
-
- - - - Bitcoin (BTC) -
-
-
0.25
-
$7,500 (at $30,000 per BTC)
-
-
-
-
- - - - - Ethereum -
-
-
1.5
-
$3,000 (at $2,000 per ETH)
-
-
-
-
- - - - - - - - - Dogeco -
-
-
5,000
-
$500 (at $0.10 per DOGE)
-
-
-
-
- - - - Litecoin (LTC) -
-
-
0.5
-
$100 (at $200 per LTC)
-
-
-
-
-
- - - - -
-
-
+
+
0.5
+
+ $100 (at $200 per LTC) +
+
+
+
+
+ + + +
-
+
+
+
- ) -} \ No newline at end of file +
+ + ); +} diff --git a/components/depth/AskTable.tsx b/components/depth/AskTable.tsx index 3e60f44..701e4fd 100644 --- a/components/depth/AskTable.tsx +++ b/components/depth/AskTable.tsx @@ -1,47 +1,43 @@ +import { Table, TableCell, TableHead, TableHeader, TableRow, TableBody } from "@/components/ui/table" -export const AskTable = ({ asks }: { asks: [string, string][] }) => { - let currentTotal = 0; - const relevantAsks = asks.slice(0, 15); - relevantAsks.reverse(); - const asksWithTotal: [string, string, number][] = relevantAsks.map(([price, quantity]) => [price, quantity, currentTotal += Number(quantity)]); - const maxTotal = relevantAsks.reduce((acc, [_, quantity]) => acc + Number(quantity), 0); - return
- {Object(asksWithTotal).map(([price, quantity, total]:[string,string,number]) => )} -
+interface AskOrder { + price: string; + quantity: string; } -function Ask({price, quantity, total, maxTotal}: {price: string, quantity: string, total: number, maxTotal: number}) { - return
-
-
-
- {price} -
-
- {quantity} -
-
- {total?.toFixed(2)} -
-
-
+ +interface AskProps { + asks: Map; +} + +export function Ask({ asks }: AskProps) { + const sortedAsks = Array.from(asks.entries()) + .sort((a, b) => parseFloat(b[0]) - parseFloat(a[0])) + .slice(0, 10) + .map(([price, quantity]): AskOrder => ({ price, quantity })); + + const maxQuantity = Math.max(...sortedAsks.map(ask => parseFloat(ask.quantity))); + + return ( + + + + Price + Quantity + + + + {sortedAsks.map((ask) => ( + + {parseFloat(ask.price).toFixed(2)} + {parseFloat(ask.quantity).toFixed(4)} +
+ ) } \ No newline at end of file diff --git a/components/depth/BidTable.tsx b/components/depth/BidTable.tsx index 7102eaa..54ad712 100644 --- a/components/depth/BidTable.tsx +++ b/components/depth/BidTable.tsx @@ -1,50 +1,35 @@ +import { Table, TableCell, TableRow, TableBody } from "@/components/ui/table" -export const BidTable = ({ bids }: {bids: [string, string][]}) => { - let currentTotal = 0; - const relevantBids = bids.slice(0, 15); - const bidsWithTotal: [string, string, number][] = relevantBids.map(([price, quantity]) => [price, quantity, currentTotal += Number(quantity)]); - const maxTotal = relevantBids.reduce((acc, [_, quantity]) => acc + Number(quantity), 0); - - return
- {bidsWithTotal?.map(([price, quantity, total]) => )} -
+interface BidProps { + bids: Map; } -function Bid({ price, quantity, total, maxTotal }: { price: string, quantity: string, total: number, maxTotal: number }) { - return ( -
-
-
-
- {price} -
-
- {quantity} -
-
- {total.toFixed(2)} -
-
-
- ); -} +export function Bid({ bids }: BidProps) { + const sortedBids = Array.from(bids.entries()) + .sort((a, b) => parseFloat(b[0]) - parseFloat(a[0])) + .slice(0, 10); + + const maxQuantity = Math.max(...sortedBids.map(([_, quantity]) => parseFloat(quantity))); + + return ( + + + {sortedBids.map(([price, quantity]) => ( + + + {parseFloat(price).toFixed(2)} + + + {parseFloat(quantity).toFixed(4)} + +
+ ) +} \ No newline at end of file diff --git a/components/depth/Depth.tsx b/components/depth/Depth.tsx deleted file mode 100644 index 58189aa..0000000 --- a/components/depth/Depth.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; -import { useEffect, useState } from "react"; -import { getDepth, getKlines, getTicker, getTrades } from "../../app/utils/ServerProps"; -import { BidTable } from "./BidTable"; -import { AskTable } from "./AskTable"; -import { SignalingManager } from "@/app/utils/SignalingManager"; -import { removeZeroEntries } from "@/app/utils/Algorithms"; - -interface DepthProps { - market: string; -} - -interface DepthData { - bids: [string, string][]; - asks: [string, string][]; -} - -export function Depth({ market }: DepthProps) { - const [bids, setBids] = useState<[string, string][]>(); - const [asks, setAsks] = useState<[string, string][]>(); - const [price, setPrice] = useState(); - - useEffect(() => { - const signalingManager = SignalingManager.getInstance(); - - const depthCallback = (data: DepthData) => { - setBids((originalBids) => { - const bidsAfterUpdate = [...(originalBids || [])]; - - for (let i = 0; i < bidsAfterUpdate.length; i++) { - for (let j = 0; j < data.bids.length; j++) { - if (bidsAfterUpdate[i][0] === data.bids[j][0]) { - bidsAfterUpdate[i][1] = data.bids[j][1]; - break; - } - } - } - return bidsAfterUpdate; - }); - - setAsks((originalAsks) => { - const asksAfterUpdate = [...(originalAsks || [])]; - - for (let i = 0; i < asksAfterUpdate.length; i++) { - for (let j = 0; j < data.asks.length; j++) { - if (asksAfterUpdate[i][0] === data.asks[j][0]) { - asksAfterUpdate[i][1] = data.asks[j][1]; - break; - } - } - } - const updatedAsks = removeZeroEntries(asksAfterUpdate); - return updatedAsks; - }); - }; - - signalingManager.registerCallback("depth", depthCallback, `DEPTH-${market}`); - signalingManager.sendMessage({ "method": "SUBSCRIBE", "params": [`depth.200ms.${market}`] }); - - getDepth(market).then(d => { - setBids(d.bids.reverse()); - setAsks(d.asks); - }); - - getTicker(market).then(t => setPrice(t.lastPrice)); - getTrades(market).then(t => setPrice(t[0].price)); - - return () => { - signalingManager.sendMessage({ "method": "UNSUBSCRIBE", "params": [`depth.${market}`] }); - signalingManager.deRegisterCallback("depth", `DEPTH-${market}`); - }; - }, [market]); - - return ( -
- - {asks && } - {price &&
{price}
} - {bids && } -
- ); -} - -function TableHeader() { - return ( -
-
Price
-
Size
-
Total
-
- ); -} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/db/prisma/migrations/20240916113127_init56/migration.sql b/db/prisma/migrations/20240916113127_init56/migration.sql new file mode 100644 index 0000000..2f77ec7 --- /dev/null +++ b/db/prisma/migrations/20240916113127_init56/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "password" TEXT; diff --git a/db/prisma/migrations/20240922143806_init34/migration.sql b/db/prisma/migrations/20240922143806_init34/migration.sql new file mode 100644 index 0000000..d259816 --- /dev/null +++ b/db/prisma/migrations/20240922143806_init34/migration.sql @@ -0,0 +1,62 @@ +/* + Warnings: + + - The primary key for the `Account` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `Account` table. All the data in the column will be lost. + - The primary key for the `Session` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `Session` table. All the data in the column will be lost. + - You are about to drop the column `password` on the `User` table. All the data in the column will be lost. + - Added the required column `updatedAt` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "Account_provider_providerAccountId_key"; + +-- DropIndex +DROP INDEX "VerificationToken_identifier_token_key"; + +-- DropIndex +DROP INDEX "VerificationToken_token_key"; + +-- AlterTable +ALTER TABLE "Account" DROP CONSTRAINT "Account_pkey", +DROP COLUMN "id", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL, +ADD CONSTRAINT "Account_pkey" PRIMARY KEY ("provider", "providerAccountId"); + +-- AlterTable +ALTER TABLE "Session" DROP CONSTRAINT "Session_pkey", +DROP COLUMN "id", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "User" DROP COLUMN "password", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "VerificationToken" ADD CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier", "token"); + +-- CreateTable +CREATE TABLE "Authenticator" ( + "credentialID" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "credentialPublicKey" TEXT NOT NULL, + "counter" INTEGER NOT NULL, + "credentialDeviceType" TEXT NOT NULL, + "credentialBackedUp" BOOLEAN NOT NULL, + "transports" TEXT, + + CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID"); + +-- AddForeignKey +ALTER TABLE "Authenticator" ADD CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/db/prisma/schema.prisma b/db/prisma/schema.prisma index 2f5355b..ad26b7d 100644 --- a/db/prisma/schema.prisma +++ b/db/prisma/schema.prisma @@ -1,22 +1,79 @@ -generator client { - provider = "prisma-client-js" -} - datasource db { provider = "postgresql" url = env("DATABASE_URL") } +generator client { + provider = "prisma-client-js" +} + model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String @unique + email String @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] - orders Order[] - cryptos Crypto[] + Authenticator Authenticator[] + crypto Crypto[] + order Order[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Account { + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([provider, providerAccountId]) +} + +model Session { + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model VerificationToken { + identifier String + token String + expires DateTime + + @@id([identifier, token]) +} + +model Authenticator { + credentialID String @unique + userId String + providerAccountId String + credentialPublicKey String + counter Int + credentialDeviceType String + credentialBackedUp Boolean + transports String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([userId, credentialID]) } model Order { @@ -43,37 +100,3 @@ model Crypto { userId String user User @relation(fields: [userId], references: [id]) } - -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} \ No newline at end of file diff --git a/hooks/MarketWebsockets.ts b/hooks/MarketWebsockets.ts new file mode 100644 index 0000000..36ce5fb --- /dev/null +++ b/hooks/MarketWebsockets.ts @@ -0,0 +1,27 @@ +import { MarketData } from "@/lib/types"; +import React, { useEffect, useState } from "react"; + +export default function UseMarketWebsockets(market:string) { + const [marketData, setMarketData] = useState(null); + const [priceChangeColor, setPriceChangeColor] = useState< + "text-green-500" | "text-red-500" + >("text-green-500"); + + useEffect(() => { + const ws = new WebSocket( + `wss://stream.binance.com:9443/ws/${market}@ticker` + ); + + ws.onmessage = (event) => { + const data: MarketData = JSON.parse(event.data); + setMarketData(data); + setPriceChangeColor( + parseFloat(data.P) >= 0 ? "text-green-500" : "text-red-500" + ); + }; + + return () => ws.close(); + }, [market]); + + return { marketData , priceChangeColor } +} diff --git a/lib/auth.ts b/lib/auth.ts index e69de29..897f495 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -0,0 +1,16 @@ +import GoogleProvider from "next-auth/providers/google"; +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import { prisma } from "./prisma"; + +export const authOptions = { + adapter: PrismaAdapter(prisma), + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], + pages: { + signIn: "/auth/signin", + } +}; diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..689c518 --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from "@prisma/client" + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient } + +export const prisma = globalForPrisma.prisma || new PrismaClient() + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..a6c94ee --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,10 @@ +export type MarketData = { + s: string; + c: string; + p: string; + P: string; + h: string; + l: string; + v: string; + q: string; +}; diff --git a/lib/utils.ts b/lib/utils.ts index 7c01fd4..fcb2755 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,8 +1,8 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } export function getUserLogin() { @@ -11,6 +11,5 @@ export function getUserLogin() { if (userToken !== null) return true; return false; } - return false; + return false; } - diff --git a/package-lock.json b/package-lock.json index 16039ed..55e1d49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "binance-frontend", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.9.0", "@next-auth/mongodb-adapter": "^1.1.3", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.19.1", @@ -17,8 +18,11 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@tanstack/react-query": "^5.51.11", + "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", "autoprefixer": "^10.4.19", "axios": "^1.7.2", @@ -27,7 +31,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "framer-motion": "^11.3.31", - "lightweight-charts": "^4.1.6", + "lightweight-charts": "^4.2.0", "lucide-react": "^0.407.0", "mongodb": "^5.9.2", "next": "14.2.4", @@ -40,10 +44,13 @@ "react-draggable": "^4.4.6", "react-hook-form": "^7.53.0", "react-query": "^3.39.3", + "react-use-websocket": "^4.8.1", + "recharts": "^2.12.7", "sharp": "^0.33.5", "short-uuid": "^5.2.0", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", + "yup": "^1.4.0", "zod": "^3.23.8" }, "devDependencies": { @@ -178,6 +185,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz", "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==" }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1418,6 +1433,63 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.0.tgz", + "integrity": "sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz", + "integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz", @@ -1513,6 +1585,20 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", @@ -1618,11 +1704,73 @@ "react": "^18.0.0" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==" }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2597,8 +2745,117 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -2673,6 +2930,11 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -2810,6 +3072,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3448,6 +3719,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/fancy-canvas": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", @@ -3459,6 +3735,14 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -4118,6 +4402,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4693,9 +4985,9 @@ } }, "node_modules/lightweight-charts": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.1.6.tgz", - "integrity": "sha512-6NLRhYGSOorEXQireN+/Fh25lU2vzvHAcPujQZOqQGB/QGereMsoyLhuX5mz4odXune01mKhhmd8UTYIgRDGqg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.0.tgz", + "integrity": "sha512-TRC61cI6bEKBEHcpqawL0KP8Rwos9b2OcdyeQ4Ri7Zybf8NvrRYmNcJV+2a2rMo7VZp1NhVWURuNEskjc+wm4A==", "dependencies": { "fancy-canvas": "2.1.0" } @@ -4728,6 +5020,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5810,6 +6107,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5976,6 +6278,20 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -5998,6 +6314,30 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-use-websocket": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.8.1.tgz", + "integrity": "sha512-FTXuG5O+LFozmu1BRfrzl7UIQngECvGJmL7BHsK4TYXuVt+mCizVA8lT0hGSIF0Z0TedF7bOo1nRzOUdginhDw==", + "peerDependencies": { + "react": ">= 18.0.0", + "react-dom": ">= 18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6030,6 +6370,36 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -6842,6 +7212,16 @@ "node": ">=0.8" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6853,6 +7233,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7127,6 +7512,27 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -7389,6 +7795,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index 74af536..020231b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@next-auth/mongodb-adapter": "^1.1.3", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.19.1", @@ -18,8 +19,11 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@tanstack/react-query": "^5.51.11", + "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", "autoprefixer": "^10.4.19", "axios": "^1.7.2", @@ -28,7 +32,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "framer-motion": "^11.3.31", - "lightweight-charts": "^4.1.6", + "lightweight-charts": "^4.2.0", "lucide-react": "^0.407.0", "mongodb": "^5.9.2", "next": "14.2.4", @@ -41,10 +45,13 @@ "react-draggable": "^4.4.6", "react-hook-form": "^7.53.0", "react-query": "^3.39.3", + "react-use-websocket": "^4.8.1", + "recharts": "^2.12.7", "sharp": "^0.33.5", "short-uuid": "^5.2.0", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", + "yup": "^1.4.0", "zod": "^3.23.8" }, "devDependencies": {