Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function login(_previousState: string, formData: FormData): Promise
console.log(`User ${username} logged in successfully`);

// Redirect to dashboard after successful login
redirect('/dashboard');
redirect('/global-assets/lookup');
} catch (error) {
// Type narrowing for the axios error
if (axios.isAxiosError(error)) {
Expand Down
24 changes: 24 additions & 0 deletions src/app/(dashboard)/_components/BalanceDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// app/(dashboard)/_components/BalanceDisplay.tsx
'use client';

import {useBalance} from '@/context/BalanceContext';
import {CircleDollarSign} from 'lucide-react';

export default function BalanceDisplay() {
const {balance, isLoading} = useBalance();

return (
<div className="flex items-center gap-2 px-3 py-1 rounded-md bg-muted text-muted-foreground dark:bg-muted/50">
<CircleDollarSign size={18} className="text-primary"/>
{isLoading ? (
<span className="text-sm font-medium text-muted-foreground animate-pulse">
Loading...
</span>
) : (
<span className="text-sm font-semibold text-foreground">
${balance?.toFixed(2)}
</span>
)}
</div>
);
}
253 changes: 253 additions & 0 deletions src/app/(dashboard)/_components/HeaderSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
'use client'

import {useRouter} from "next/navigation";
import {Button} from "@/components/ui/button";
import {ScrollArea} from "@/components/ui/scroll-area";
import {useEffect, useState} from "react";
import {useDebounce} from "use-debounce";
import {SearchResult} from "../global-assets/lookup/_utils/definitions";
import {searchYahooFinance} from "@/app/(dashboard)/global-assets/lookup/_utils/actions";
import {Popover} from "@radix-ui/react-popover";
import {PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {Input} from "@/components/ui/input";
import {Loader2, Search, X} from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import {Badge} from "@/components/ui/badge";

function HeaderSearchBar({className = ''}: { className?: string }) {
const [searchQuery, setSearchQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [searchResults, setSearchResults] = useState<SearchResult | null>(null);
const [error, setError] = useState<string | null>(null);
const router = useRouter();

// Get the first value from the array returned by useDebounce
const [debouncedQuery] = useDebounce(searchQuery, 1000);

// Function to fetch search results
const fetchSearchResults = async (query: string) => {
if (query.length < 2) {
setIsOpen(false);
return;
}

setIsLoading(true);
setError(null);

try {
const response = await searchYahooFinance({
query,
newsCount: 3,
quoteCount: 4
});

if (response.success) {
setSearchResults(response.data);
setIsOpen(true);
} else {
setError(response.error);
setSearchResults(null);
}
} catch (err) {
console.error(err);
setError("An unexpected error occurred");
setSearchResults(null);
} finally {
setIsLoading(false);
}
};

// Handle debounced query changes
useEffect(() => {
if (debouncedQuery.length >= 2) {
fetchSearchResults(debouncedQuery).then();
} else {
setIsOpen(false);
}
}, [debouncedQuery]);

// Handle view all results click
const handleViewAllResults = () => {
if (searchQuery) {
router.push(`/global-assets/lookup?query=${encodeURIComponent(searchQuery)}`);
setIsOpen(false);
}
};

const handleClearSearch = () => {
setSearchQuery("");
setIsOpen(false);
}

return (
<div className={`relative ${className}`}>
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<div className="relative w-full">
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search stocks..."
className="w-full h-9 pl-9 pr-8 text-sm focus-visible:ring-1"
/>
{isLoading ? (
<Loader2
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 animate-spin"/>
) : (
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400"/>
)}
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0"
onClick={handleClearSearch}
>
<X className="h-3 w-3 text-gray-400"/>
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="w-[320px] md:w-[400px] p-0 max-h-[60vh] overflow-auto"
align="end"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<ScrollArea>
<HeaderSearchResults
data={searchResults}
error={error}
onViewAllResults={handleViewAllResults}
/>
</ScrollArea>
</PopoverContent>
</Popover>
</div>
);
}

function HeaderSearchResults({
data,
error,
onViewAllResults
}: {
data: SearchResult | null;
error: string | null;
onViewAllResults: () => void;
}) {
if (error) {
return (
<div className="p-3 text-xs text-destructive bg-destructive/10">
Error fetching results: {error}
</div>
);
}

const {quotes = [], news = []} = data || {};
const hasResults = quotes.length > 0 || news.length > 0;

if (!hasResults) {
return (
<div className="p-3 text-center text-xs text-muted-foreground">
No results found. Try a different search term.
</div>
);
}

return (
<div className="p-3 space-y-3">
{/* Quotes Section */}
{quotes.length > 0 && (
<div>
<h3 className="text-xs font-medium text-muted-foreground mb-1">Stocks & Companies</h3>
<div className="space-y-1">
{quotes.map((quote) => (
<Link
href={`/assets/${quote.symbol}`}
key={quote.symbol}
className="flex items-center justify-between p-1.5 hover:bg-muted rounded-sm transition-colors cursor-pointer"
>
<div className="flex items-center gap-2">
<span className="font-medium text-primary text-xs">{quote.symbol}</span>
<span className="text-xs truncate max-w-[150px]">{quote.shortName || 'N/A'}</span>
</div>
<div className="text-xs text-muted-foreground">
{quote.exchange || 'N/A'}
</div>
</Link>
))}
</div>
</div>
)}

{/* News Section */}
{news.length > 0 && (
<div>
<h3 className="text-xs font-medium text-muted-foreground mb-1">Latest News</h3>
<div className="space-y-2">
{news.map((item) => (
<Link
href={item.link}
key={item.uuid}
target="_blank"
rel="noopener noreferrer"
className="block p-1.5 hover:bg-muted rounded-sm transition-colors"
>
<div className="flex gap-2">
{item.thumbnail && (
<div
className="relative w-10 h-10 flex-shrink-0 bg-muted rounded overflow-hidden">
<Image
src={item.thumbnail}
alt={item.title}
fill
className="object-cover"
sizes="40px"
/>
</div>
)}
<div className="flex-1">
<p className="text-xs line-clamp-2">{item.title}</p>
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground">{item.publisher}</span>
{item.relatedTickers && item.relatedTickers.length > 0 && (
<div className="flex gap-1">
{item.relatedTickers.slice(0, 1).map((ticker) => (
<Badge key={ticker} variant="outline"
className="text-xs px-1 py-0 h-4">
{ticker}
</Badge>
))}
{item.relatedTickers.length > 1 && (
<span className="text-xs text-muted-foreground">
+{item.relatedTickers.length - 1}
</span>
)}
</div>
)}
</div>
</div>
</div>
</Link>
))}
</div>
</div>
)}

{/* View all results link */}
<div className="text-center border-t border-gray-200 dark:border-gray-700 pt-2">
<button
onClick={onViewAllResults}
className="text-xs text-primary hover:underline p-1.5"
>
View all results
</button>
</div>
</div>
);
}

export default HeaderSearchBar;
16 changes: 16 additions & 0 deletions src/app/(dashboard)/_components/LoadingAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

const LoadingAnimation = () => {
return (
<div className="flex items-center justify-center h-full w-full">
<div className="loading-wave">
<div className="loading-bar"></div>
<div className="loading-bar"></div>
<div className="loading-bar"></div>
<div className="loading-bar"></div>
</div>
</div>
);
};

export default LoadingAnimation;
49 changes: 18 additions & 31 deletions src/app/(dashboard)/_components/app-sidebar-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,13 @@ import {SidebarData} from "@/app/(dashboard)/_utils/types";
import {
ChartCandlestickIcon, DollarSign,
Earth,
FolderGit2,
LayoutGrid,
PieChart,
Users,
ChartColumnIncreasing
} from "lucide-react";

const userSidebar: SidebarData = {
navMain: [
{
title: "Dashboard",
url: "/dashboard",
icon: LayoutGrid,
},
{
title: "Global Market",
url: '/assets',
Expand Down Expand Up @@ -62,28 +55,11 @@ const userSidebar: SidebarData = {
title: "StockMarket Prediction",
url: "/dashboard/stockmarketprediction",
icon: ChartColumnIncreasing
}],
guides: [
{
name: "ML Model Notebook",
url: "/notebooks",
icon: FolderGit2,
},
{
name: "Documentation",
url: "/docs",
icon: Users,
},
],
}]
}

const adminSidebar: SidebarData = {
navMain: [
{
title: "Dashboard",
url: "/dashboard",
icon: LayoutGrid,
},
{
title: "Global Assets",
url: '/assets',
Expand All @@ -109,14 +85,24 @@ const adminSidebar: SidebarData = {
title: "System Assets",
url: '/assets/db',
icon: ChartCandlestickIcon,
},
{
title: "Budget Tracker",
url: "/dashboard/budget",
icon: DollarSign,
},
{
title: "Portfolio Optimization",
url: "/dashboard/portfolio",
icon: PieChart
},
{
title: "StockMarket Prediction",
url: "/dashboard/stockmarketprediction",
icon: ChartColumnIncreasing
}
],
guides: [
{
name: "ML Model Notebook",
url: "/notebooks",
icon: FolderGit2,
},
{
name: "Documentation",
url: "/docs",
Expand All @@ -130,7 +116,8 @@ const AppSidebarLinks = ({role}: { role: Role }) => {
return (
<>
<NavMain items={sidebar.navMain}/>
<NavGuides projects={sidebar.guides}/>
{sidebar.guides ?
<NavGuides projects={sidebar.guides}/> : null}
</>
);
};
Expand Down
Loading