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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 436 additions & 5 deletions frontend/package-lock.json

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,26 @@
"notifications:ws": "node scripts/ws-notification-server.js"
},
"dependencies": {
"@tanstack/react-query": "^5.90.21",
"clsx": "^2.1.0",
"framer-motion": "^11.0.3",
"lucide-react": "^0.330.0",
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"lucide-react": "^0.330.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.1",
"framer-motion": "^11.0.3"
"recharts": "^3.7.0",
"tailwind-merge": "^2.2.1"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"ws": "^8.18.0"
}
}
13 changes: 13 additions & 0 deletions frontend/src/app/dashboard/hooks/useAnalyticsData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import api from '../lib/api/';

export function useAnalyticsData() {
return useQuery({
queryKey: ['analyticsData'],
queryFn: async () => {
const res = await api.get('/api/analytics/performance');
return res.data;
},
refetchInterval: 5000, // real-time updates
});
}
12 changes: 12 additions & 0 deletions frontend/src/app/dashboard/hooks/usePredictiveModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import api from '../lib/api/client';

export function usePredictiveModels() {
return useQuery({
queryKey: ['predictiveInsights'],
queryFn: async () => {
const res = await api.get('/api/analytics/predictive');
return res.data;
},
});
}
125 changes: 55 additions & 70 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
"use client";
'use client';

import React, { useState, useEffect } from "react";
import { useAnalyticsData } from "../hooks/useAnalyticsData";
import { usePredictiveModels } from "../hooks/usePredictiveModels";

// Analytics Components
import { PerformanceChart } from "./components/PerformanceChart";
import { RiskMetrics } from "./components/RiskMetrics";
import { PredictiveInsights } from "./components/PredictiveInsights";
import { MarketTrends } from "./components/MarketTrends";

// Portfolio Components
import Button from "@/components/ui/Button";
import InvestmentTable from "@/components/InvestmentTable";
import PortfolioStats from "@/components/PortfolioStats";
import PortfolioChart from "@/components/PortfolioChart";
import LoadingDashboard from "@/components/LoadingDashboard";


// Mock data types
// Types
interface Investment {
id: string;
projectName: string;
Expand All @@ -28,7 +37,7 @@ interface PortfolioData {
investments: Investment[];
}

// Mock data
// Mock data for portfolio
const mockPortfolioData: PortfolioData = {
totalInvested: 15000,
totalCurrentValue: 18500,
Expand Down Expand Up @@ -79,56 +88,51 @@ const mockPortfolioData: PortfolioData = {
};

export default function DashboardPage() {
// Portfolio state
const [portfolioData, setPortfolioData] =
useState<PortfolioData>(mockPortfolioData);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingPortfolio, setIsLoadingPortfolio] = useState(true);
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState("");

// Simulate loading
// Analytics state
const { data: analyticsData, isLoading: isLoadingAnalytics } = useAnalyticsData();
const { data: predictiveData } = usePredictiveModels();

// Simulate portfolio loading
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
setIsLoadingPortfolio(false);
}, 1000);
return () => clearTimeout(timer);
}, []);

const handleClaim = async (investmentId: string, amount: number) => {
try {
// Mock claim process
await new Promise((resolve) => setTimeout(resolve, 1500));

// Update portfolio data
setPortfolioData((prev) => ({
...prev,
investments: prev.investments.map((inv) =>
inv.id === investmentId
? { ...inv, claimableReturns: 0, canClaim: false }
: inv,
: inv
),
totalClaimableReturns: prev.totalClaimableReturns - amount,
}));

// Show success toast
setToastMessage(`Successfully claimed $${amount.toLocaleString()}!`);
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
// Emit real-time notification (notification center + other tabs)
try {
await fetch("/api/notifications/emit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "contribution_confirmation",
title: "Returns claimed",
message: `Successfully claimed $${amount.toLocaleString()} from your investment.`,
link: "/dashboard",
}),
});
} catch {
// ignore
}
} catch (error) {
await fetch("/api/notifications/emit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "contribution_confirmation",
title: "Returns claimed",
message: `Successfully claimed $${amount.toLocaleString()} from your investment.`,
link: "/dashboard",
}),
});
} catch {
setToastMessage("Failed to claim returns. Please try again.");
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
Expand All @@ -137,13 +141,12 @@ export default function DashboardPage() {

const hasInvestments = portfolioData.investments.length > 0;

if (isLoading) {
if (isLoadingPortfolio || isLoadingAnalytics) {
return <LoadingDashboard />;
}

return (
<>
{/* Toast Notification */}
{showToast && (
<div className="fixed top-20 right-4 z-50 bg-gradient-to-r from-green-600 to-emerald-600 text-white px-6 py-3 rounded-lg shadow-lg animate-fade-in backdrop-blur-sm border border-white/20">
{toastMessage}
Expand All @@ -152,70 +155,52 @@ export default function DashboardPage() {

<div className="container mx-auto px-4 py-8 pt-16">
<div className="mb-10">
<div className="inline-block px-4 py-1.5 rounded-full bg-gradient-to-r from-purple-600/20 to-indigo-600/20 text-purple-300 text-sm font-medium mb-4">
Investor Dashboard
</div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent mb-4">
Portfolio Overview
Unified Dashboard
</h1>
<p className="text-slate-400 max-w-2xl">
Track your investments, monitor returns, and manage your portfolio with real-time insights.
Track your portfolio, monitor returns, and gain predictive insights with advanced analytics.
</p>
</div>

<div className="mb-8">
{hasInvestments && (
<Button
onClick={() => (window.location.href = "/explore")}
className="bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 shadow-sm hover:shadow-md"
>
Explore More Projects
</Button>
)}
</div>

{/* Portfolio Section */}
{hasInvestments ? (
<>
{/* Portfolio Stats */}
<PortfolioStats data={portfolioData} />

{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
{/* Investment Table */}
<div className="lg:col-span-2">
<InvestmentTable
investments={portfolioData.investments}
onClaim={handleClaim}
/>
</div>

{/* Portfolio Chart */}
<div className="lg:col-span-1">
<PortfolioChart investments={portfolioData.investments} />
</div>
</div>
</>
) : (
<div className="text-center py-16 px-4">
<div className="mx-auto max-w-md">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-slate-800 to-slate-900 border border-white/10 mb-6">
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-2xl font-bold text-white mb-2">No Investments Yet</h3>
<p className="text-slate-400 mb-8 max-w-sm mx-auto">
Start building your portfolio by exploring and investing in impactful projects.
</p>
<Button
onClick={() => (window.location.href = "/explore")}
className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white hover:from-purple-500 hover:to-indigo-500"
>
Explore Projects
</Button>
</div>
<h3 className="text-2xl font-bold text-white mb-2">No Investments Yet</h3>
<p className="text-slate-400 mb-8 max-w-sm mx-auto">
Start building your portfolio by exploring and investing in impactful projects.
</p>
<Button
onClick={() => (window.location.href = "/explore")}
className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white hover:from-purple-500 hover:to-indigo-500"
>
Explore Projects
</Button>
</div>
)}

{/* Analytics Section */}
<div className="mt-16 grid gap-6 md:grid-cols-2">
<PerformanceChart data={analyticsData.performance} />
<RiskMetrics metrics={analyticsData.risk} />
<PredictiveInsights insights={predictiveData?.insights || []} />
<MarketTrends data={analyticsData.trends} />
</div>
</div>
</>
);
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/MarketTrends.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

export function MarketTrends({ data }: { data: any[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<XAxis dataKey="sector" />
<YAxis />
<Tooltip />
<Bar dataKey="growthRate" fill="#82ca9d" />
</BarChart>
</ResponsiveContainer>
);
}
14 changes: 14 additions & 0 deletions frontend/src/components/PerformanceChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

export function PerformanceChart({ data }: { data: any[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="portfolioValue" stroke="#8884d8" />
</LineChart>
</ResponsiveContainer>
);
}
12 changes: 12 additions & 0 deletions frontend/src/components/PredictiveInsights.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function PredictiveInsights({ insights }: { insights: any[] }) {
return (
<div className="p-4 border rounded">
<h3 className="font-bold mb-2">Predictive Analytics</h3>
<ul>
{insights.map((i, idx) => (
<li key={idx}>{i.message} (Confidence: {i.confidence}%)</li>
))}
</ul>
</div>
);
}
9 changes: 9 additions & 0 deletions frontend/src/components/RiskMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function RiskMetrics({ metrics }: { metrics: { volatility: number; sharpeRatio: number } }) {
return (
<div className="p-4 border rounded">
<h3 className="font-bold mb-2">Risk Assessment</h3>
<p>Volatility: {metrics.volatility.toFixed(2)}%</p>
<p>Sharpe Ratio: {metrics.sharpeRatio.toFixed(2)}</p>
</div>
);
}
Loading