
No Comments Yet
Be the first to share your thoughts and start the conversation.

Be the first to share your thoughts and start the conversation.
We’ve built the foundation of our stock market app - now it’s time to take it further with a watchlist feature. This will let users add, remove and track the stocks they care about most.
Here’s the roadmap for building it:
Work through these steps yourself first. it’s the best way to strengthen your skills and solidify the concepts we’ve covered. I’ve provided the code below to guide you whenever you need a hand.
Let's start by creating the Watchlist page.
What to do:
You can check the codes below to get an idea, but try coding it yourself first — that’s when the learning really sticks.
Hint / Solution:
export const NAV_ITEMS = [
{ href: '/', label: 'Dashboard' },
{ href: '/search', label: 'Search' },
{ href: '/watchlist', label: 'Watchlist' },
];import { Star } from 'lucide-react';
import { searchStocks } from '@/lib/actions/finnhub.actions';
import SearchCommand from '@/components/SearchCommand';
const Watchlist = async () => {
const watchlist = []; // We'll get the actual watchlist items later
const initialStocks = await searchStocks();
// Empty state
if (watchlist.length === 0) {
return (
<section className="flex watchlist-empty-container">
<div className="watchlist-empty">
<Star className="watchlist-star" />
<h2 className="empty-title">Your watchlist is empty</h2>
<p className="empty-description">
Start building your watchlist by searching for stocks and clicking the star icon to add them.
</p>
</div>
<SearchCommand initialStocks={initialStocks} />
</section>
);
}
return (
<section className="watchlist">
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h2 className="watchlist-title">Watchlist</h2>
<SearchCommand initialStocks={initialStocks} />
</div>
{/* WatchlistTable */}
</div>
</section>
);
};
export default Watchlist;Next, let’s set up server actions to handle the database logic for adding and removing stocks from the watchlist.
What to do:
Create the addToWatchlist server action
Also implement the removeFromWatchlist server action to delete it for that user. Do authentication and path revalidation similarly.
Hint / Solution: addToWatchlist
'use server';
import { revalidatePath } from 'next/cache';
import { auth } from '../better-auth/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
// Add stock to watchlist
export const addToWatchlist = async (symbol: string, company: string) => {
try {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) redirect('/sign-in');
// Check if stock already exists in watchlist
const existingItem = await Watchlist.findOne({
userId: session.user.id,
symbol: symbol.toUpperCase(),
});
if (existingItem) {
return { success: false, error: 'Stock already in watchlist' };
}
// Add to watchlist
const newItem = new Watchlist({
userId: session.user.id,
symbol: symbol.toUpperCase(),
company: company.trim(),
});
await newItem.save();
revalidatePath('/watchlist');
return { success: true, message: 'Stock added to watchlist' };
} catch (error) {
console.error('Error adding to watchlist:', error);
throw new Error('Failed to add stock to watchlist');
}
};Hint / Solution: removeFromWatchlist
// Remove stock from watchlist
export const removeFromWatchlist = async (symbol: string) => {
try {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) redirect('/sign-in');
// Remove from watchlist
await Watchlist.deleteOne({
userId: session.user.id,
symbol: symbol.toUpperCase(),
});
revalidatePath('/watchlist');
return { success: true, message: 'Stock removed from watchlist' };
} catch (error) {
console.error('Error removing from watchlist:', error);
throw new Error('Failed to remove stock from watchlist');
}
};Now, let's make the Watchlist button fully functional. We'll wire up the toggleWatchlist function to the button's onClick event, implementing optimistic UI updates and a debounce to prevent rapid API calls.
What to do:
Hint / Solution:
'use client';
import { useDebounce } from '@/hooks/useDebounce';
import {
addToWatchlist,
removeFromWatchlist,
} from '@/lib/actions/watchlist.actions';
import { Star, Trash2 } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
const WatchlistButton = ({
symbol,
company,
isInWatchlist,
showTrashIcon = false,
type = 'button',
onWatchlistChange,
}: WatchlistButtonProps) => {
const [added, setAdded] = useState<boolean>(!!isInWatchlist);
const label = useMemo(() => {
if (type === 'icon') return added ? '' : '';
return added ? 'Remove from Watchlist' : 'Add to Watchlist';
}, [added, type]);
// Handle adding/removing stocks from watchlist
const toggleWatchlist = async () => {
const result = added
? await removeFromWatchlist(symbol)
: await addToWatchlist(symbol, company);
if (result.success) {
toast.success(added ? 'Removed from Watchlist' : 'Added to Watchlist', {
description: `${company} ${
added ? 'removed from' : 'added to'
} your watchlist`,
});
// Notify parent component of watchlist change for state synchronization
onWatchlistChange?.(symbol, !added);
}
};
// Debounce the toggle function to prevent rapid API calls (300ms delay)
const debouncedToggle = useDebounce(toggleWatchlist, 300);
// Click handler that provides optimistic UI updates
const handleClick = (e: React.MouseEvent) => {
// Prevent event bubbling and default behavior
e.stopPropagation();
e.preventDefault();
setAdded(!added);
debouncedToggle();
};
if (type === 'icon') {
return (
<button
title={
added
? `Remove ${symbol} from watchlist`
: `Add ${symbol} to watchlist`
}
aria-label={
added
? `Remove ${symbol} from watchlist`
: `Add ${symbol} to watchlist`
}
className={`watchlist-icon-btn ${added ? 'watchlist-icon-added' : ''}`}
onClick={handleClick}
>
<Star fill={added ? 'currentColor' : 'none'} />
</button>
);
}
return (
<button
className={`watchlist-btn ${added ? 'watchlist-remove' : ''}`}
onClick={handleClick}
>
{showTrashIcon && added ? <Trash2 /> : null}
<span>{label}</span>
</button>
);
};
export default WatchlistButton;Now that the WatchlistButton is ready, let's use it. We'll need to modify the searchStocks action to return the watchlist status of each stock and then pass that information to the WatchlistButton component within the search dialog.
What to do:
Hint / Solution: Return the stock’s actual isInWatchlist status.
'use server';
import { auth } from '../better-auth/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { getWatchlistSymbolsByEmail } from './watchlist.actions';
export const searchStocks = cache(
async (query?: string): Promise<StockWithWatchlistStatus[]> => {
try {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) redirect('/sign-in');
const userWatchlistSymbols = await getWatchlistSymbolsByEmail(
session.user.email
);
const token = process.env.FINNHUB_API_KEY ?? NEXT_PUBLIC_FINNHUB_API_KEY;
if (!token) {
// If no token, log and return empty to avoid throwing per requirements
console.error(
'Error in stock search:',
new Error('FINNHUB API key is not configured')
);
return [];
}
const trimmed = typeof query === 'string' ? query.trim() : '';
let results: FinnhubSearchResult[] = [];
if (!trimmed) {
// Fetch top 10 popular symbols' profiles
const top = POPULAR_STOCK_SYMBOLS.slice(0, 10);
const profiles = await Promise.all(
top.map(async (sym) => {
try {
const url = `${FINNHUB_BASE_URL}/stock/profile2?symbol=${encodeURIComponent(
sym
)}&token=${token}`;
// Revalidate every hour
const profile = await fetchJSON<any>(url, 3600);
return { sym, profile } as { sym: string; profile: any };
} catch (e) {
console.error('Error fetching profile2 for', sym, e);
return { sym, profile: null } as { sym: string; profile: any };
}
})
);
results = profiles
.map(({ sym, profile }) => {
const symbol = sym.toUpperCase();
const name: string | undefined =
profile?.name || profile?.ticker || undefined;
const exchange: string | undefined = profile?.exchange || undefined;
if (!name) return undefined;
const r: FinnhubSearchResult = {
symbol,
description: name,
displaySymbol: symbol,
type: 'Common Stock',
};
// We don't include exchange in FinnhubSearchResult type, so carry via mapping later using profile
// To keep pipeline simple, attach exchange via closure map stage
// We'll reconstruct exchange when mapping to final type
(r as any).__exchange = exchange; // internal only
return r;
})
.filter((x): x is FinnhubSearchResult => Boolean(x));
} else {
const url = `${FINNHUB_BASE_URL}/search?q=${encodeURIComponent(
trimmed
)}&token=${token}`;
const data = await fetchJSON<FinnhubSearchResponse>(url, 1800);
results = Array.isArray(data?.result) ? data.result : [];
}
const mapped: StockWithWatchlistStatus[] = results
.map((r) => {
const upper = (r.symbol || '').toUpperCase();
const name = r.description || upper;
const exchangeFromDisplay =
(r.displaySymbol as string | undefined) || undefined;
const exchangeFromProfile = (r as any).__exchange as
| string
| undefined;
const exchange = exchangeFromDisplay || exchangeFromProfile || 'US';
const type = r.type || 'Stock';
const item: StockWithWatchlistStatus = {
symbol: upper,
name,
exchange,
type,
isInWatchlist: userWatchlistSymbols.includes(
r.symbol.toUpperCase()
),
};
return item;
})
.slice(0, 15);
return mapped;
} catch (err) {
console.error('Error in stock search:', err);
return [];
}
}
);Hint / Solution: Update WatchlistButton
'use client';
import { useDebounce } from '@/hooks/useDebounce';
import {
addToWatchlist,
removeFromWatchlist,
} from '@/lib/actions/watchlist.actions';
import { Star, StarIcon, Stars, Trash2 } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
// Minimal WatchlistButton implementation to satisfy page requirements.
// This component focuses on UI contract only. It toggles local state and
// calls onWatchlistChange if provided. Styling hooks match globals.css.
const WatchlistButton = ({
symbol,
company,
isInWatchlist,
showTrashIcon = false,
type = 'button',
onWatchlistChange,
}: WatchlistButtonProps) => {
const [added, setAdded] = useState<boolean>(!!isInWatchlist);
const label = useMemo(() => {
if (type === 'icon') return added ? '' : '';
return added ? 'Remove from Watchlist' : 'Add to Watchlist';
}, [added, type]);
// Handle adding/removing stocks from watchlist
const toggleWatchlist = async () => {
const result = added
? await removeFromWatchlist(symbol)
: await addToWatchlist(symbol, company);
if (result.success) {
toast.success(added ? 'Removed from Watchlist' : 'Added to Watchlist', {
description: `${company} ${
added ? 'removed from' : 'added to'
} your watchlist`,
});
// Notify parent component of watchlist change for state synchronization
onWatchlistChange?.(symbol, !added);
}
};
// Debounce the toggle function to prevent rapid API calls (300ms delay)
const debouncedToggle = useDebounce(toggleWatchlist, 300);
// Click handler that provides optimistic UI updates
const handleClick = (e: React.MouseEvent) => {
// Prevent event bubbling and default behavior
e.stopPropagation();
e.preventDefault();
setAdded(!added);
debouncedToggle();
};
if (type === 'icon') {
return (
<button
title={
added
? `Remove ${symbol} from watchlist`
: `Add ${symbol} to watchlist`
}
aria-label={
added
? `Remove ${symbol} from watchlist`
: `Add ${symbol} to watchlist`
}
className={`watchlist-icon-btn ${added ? 'watchlist-icon-added' : ''}`}
onClick={handleClick}
>
<Star fill={added ? 'currentColor' : 'none'} />
</button>
);
}
return (
<button
className={`watchlist-btn ${added ? 'watchlist-remove' : ''}`}
onClick={handleClick}
>
{showTrashIcon && added ? <Trash2 /> : null}
<span>{label}</span>
</button>
);
};
export default WatchlistButton;Hint / Solution: Add WatchlistButton in SearchCommand
'use client';
import { useEffect, useState } from 'react';
import {
CommandDialog,
CommandEmpty,
CommandInput,
CommandList,
} from '@/components/ui/command';
import { Button } from '@/components/ui/button';
import { Loader2, TrendingUp } from 'lucide-react';
import Link from 'next/link';
import { searchStocks } from '@/lib/actions/finnhub.actions';
import { useDebounce } from '@/hooks/useDebounce';
import WatchlistButton from './WatchlistButton';
export default function SearchCommand({
renderAs = 'button',
label = 'Add stock',
initialStocks,
}: SearchCommandProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [stocks, setStocks] =
useState<StockWithWatchlistStatus[]>(initialStocks);
const isSearchMode = !!searchTerm.trim();
const displayStocks = isSearchMode ? stocks : stocks?.slice(0, 10);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
setOpen((v) => !v);
}
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, []);
const handleSearch = async () => {
if (!isSearchMode) return setStocks(initialStocks);
setLoading(true);
try {
const results = await searchStocks(searchTerm.trim());
setStocks(results);
} catch {
setStocks([]);
} finally {
setLoading(false);
}
};
const debouncedSearch = useDebounce(handleSearch, 300);
useEffect(() => {
debouncedSearch();
}, [searchTerm]);
const handleSelectStock = () => {
setOpen(false);
setSearchTerm('');
setStocks(initialStocks);
};
// Handle watchlist changes status change
const handleWatchlistChange = async (symbol: string, isAdded: boolean) => {
// Update current stocks
setStocks(
initialStocks?.map((stock) =>
stock.symbol === symbol ? { ...stock, isInWatchlist: isAdded } : stock
) || []
);
};
return (
<>
{renderAs === 'text' ? (
<span onClick={() => setOpen(true)} className='search-text'>
{label}
</span>
) : (
<Button onClick={() => setOpen(true)} className='search-btn'>
{label}
</Button>
)}
<CommandDialog
open={open}
onOpenChange={setOpen}
className='search-dialog'
>
<div className='search-field'>
<CommandInput
value={searchTerm}
onValueChange={setSearchTerm}
placeholder='Search stocks...'
className='search-input'
/>
{loading && <Loader2 className='search-loader' />}
</div>
<CommandList className='search-list'>
{loading ? (
<CommandEmpty className='search-list-empty'>
Loading stocks...
</CommandEmpty>
) : displayStocks?.length === 0 ? (
<div className='search-list-indicator'>
{isSearchMode ? 'No results found' : 'No stocks available'}
</div>
) : (
<ul>
<div className='search-count'>
{isSearchMode ? 'Search results' : 'Popular stocks'}
{` `}({displayStocks?.length || 0})
</div>
{displayStocks?.map((stock, i) => (
<li key={stock.symbol} className='search-item'>
<Link
href={`/stocks/${stock.symbol}`}
onClick={handleSelectStock}
className='search-item-link'
>
<TrendingUp className='h-4 w-4 text-gray-500' />
<div className='flex-1'>
<div className='search-item-name'>{stock.name}</div>
<div className='text-sm text-gray-500'>
{stock.symbol} | {stock.exchange} | {stock.type}
</div>
</div>
<WatchlistButton
symbol={stock.symbol}
company={stock.name}
isInWatchlist={stock.isInWatchlist}
onWatchlistChange={handleWatchlistChange}
/>
</Link>
</li>
))}
</ul>
)}
</CommandList>
</CommandDialog>
</>
);
}Next, we’ll integrate the reusable WatchlistButton on the stock details page, allowing users to add or remove a stock from their watchlist directly while viewing its details.
Before we add the WatchlistButton, it needs access to the stock’s details and its watchlist status. To handle this, let’s create two server actions: getStockDetails and getUserWatchlist.
What to do:
Hint / Solution: getStocksDetails
// Fetch stock details by symbol
export const getStocksDetails = cache(async (symbol: string) => {
const cleanSymbol = symbol.trim().toUpperCase();
try {
const [quote, profile, financials] = await Promise.all([
fetchJSON(
// Price data - no caching for accuracy
`${FINNHUB_BASE_URL}/quote?symbol=${cleanSymbol}&token=${NEXT_PUBLIC_FINNHUB_API_KEY}`
),
fetchJSON(
// Company info - cache 1hr (rarely changes)
`${FINNHUB_BASE_URL}/stock/profile2?symbol=${cleanSymbol}&token=${NEXT_PUBLIC_FINNHUB_API_KEY}`,
3600
),
fetchJSON(
// Financial metrics (P/E, etc.) - cache 30min
`${FINNHUB_BASE_URL}/stock/metric?symbol=${cleanSymbol}&metric=all&token=${NEXT_PUBLIC_FINNHUB_API_KEY}`,
1800
),
]);
// Type cast the responses
const quoteData = quote as QuoteData;
const profileData = profile as ProfileData;
const financialsData = financials as FinancialsData;
// Check if we got valid quote and profile data
if (!quoteData?.c || !profileData?.name)
throw new Error('Invalid stock data received from API');
const changePercent = quoteData.dp || 0;
const peRatio = financialsData?.metric?.peNormalizedAnnual || null;
return {
symbol: cleanSymbol,
company: profileData?.name,
currentPrice: quoteData.c,
changePercent,
priceFormatted: formatPrice(quoteData.c),
changeFormatted: formatChangePercent(changePercent),
peRatio: peRatio?.toFixed(1) || '—',
marketCapFormatted: formatMarketCapValue(
profileData?.marketCapitalization || 0
),
};
} catch (error) {
console.error(`Error fetching details for ${cleanSymbol}:`, error);
throw new Error('Failed to fetch stock details');
}
});Finnhub splits different kinds of stock information across separate endpoints, so to get everything we need, we’ll call three different APIs and combine the results.
Here are the key metrics we’ll be pulling:
Next, let’s create the getUserWatchlist server action to fetch the user’s watchlist along with each stock’s status. This will allow our WatchlistButton to display the correct state.
Hint / Solution: getUserWatchlist
// Get user's watchlist
export const getUserWatchlist = async () => {
try {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) redirect('/sign-in');
const watchlist = await Watchlist.find({ userId: session.user.id })
.sort({ addedAt: -1 })
.lean();
return JSON.parse(JSON.stringify(watchlist));
} catch (error) {
console.error('Error fetching watchlist:', error);
throw new Error('Failed to fetch watchlist');
}
}Hint / Solution: WatchlistButton in stock details page
import TradingViewWidget from '@/components/TradingViewWidget';
import WatchlistButton from '@/components/WatchlistButton';
import { WatchlistItem } from '@/database/models/watchlist.model';
import { getStocksDetails } from '@/lib/actions/finnhub.actions';
import { getUserWatchlist } from '@/lib/actions/watchlist.actions';
import {
SYMBOL_INFO_WIDGET_CONFIG,
CANDLE_CHART_WIDGET_CONFIG,
BASELINE_WIDGET_CONFIG,
TECHNICAL_ANALYSIS_WIDGET_CONFIG,
COMPANY_PROFILE_WIDGET_CONFIG,
COMPANY_FINANCIALS_WIDGET_CONFIG,
} from '@/lib/constants';
import { notFound } from 'next/navigation';
export default async function StockDetails({ params }: StockDetailsPageProps) {
const { symbol } = await params;
const scriptUrl = `https://s3.tradingview.com/external-embedding/embed-widget-`;
const stockData = await getStocksDetails(symbol.toUpperCase());
const watchlist = await getUserWatchlist();
const isInWatchlist = watchlist.some(
(item: WatchlistItem) => item.symbol === symbol.toUpperCase()
);
if (!stockData) notFound();
return (
<div className='flex min-h-screen p-4 md:p-6 lg:p-8'>
<section className='grid grid-cols-1 md:grid-cols-2 gap-8 w-full'>
{/* Left column */}
<div className='flex flex-col gap-6'>
<TradingViewWidget
scriptUrl={`${scriptUrl}symbol-info.js`}
config={SYMBOL_INFO_WIDGET_CONFIG(symbol)}
height={170}
/>
<TradingViewWidget
scriptUrl={`${scriptUrl}advanced-chart.js`}
config={CANDLE_CHART_WIDGET_CONFIG(symbol)}
className='custom-chart'
height={600}
/>
<TradingViewWidget
scriptUrl={`${scriptUrl}advanced-chart.js`}
config={BASELINE_WIDGET_CONFIG(symbol)}
className='custom-chart'
height={600}
/>
</div>
{/* Right column */}
<div className='flex flex-col gap-6'>
<div className='flex items-center justify-between'>
<WatchlistButton
symbol={symbol}
company={stockData.company}
isInWatchlist={isInWatchlist}
type='button'
/>
</div>
<TradingViewWidget
scriptUrl={`${scriptUrl}technical-analysis.js`}
config={TECHNICAL_ANALYSIS_WIDGET_CONFIG(symbol)}
height={400}
/>
<TradingViewWidget
scriptUrl={`${scriptUrl}company-profile.js`}
config={COMPANY_PROFILE_WIDGET_CONFIG(symbol)}
height={440}
/>
<TradingViewWidget
scriptUrl={`${scriptUrl}financials.js`}
config={COMPANY_FINANCIALS_WIDGET_CONFIG(symbol)}
height={464}
/>
</div>
</section>
</div>
);
}Now that we can add and remove data from our watchlist, let’s display the stocks in a WatchlistTable.
What to do:
Hint / Solution: getWatchlistWithData
// Get user's watchlist with stock data
export const getWatchlistWithData = async () => {
try {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) redirect('/sign-in');
const watchlist = await Watchlist.find({ userId: session.user.id }).sort({ addedAt: -1 }).lean();
if (watchlist.length === 0) return [];
const stocksWithData = await Promise.all(
watchlist.map(async (item) => {
const stockData = await getStocksDetails(item.symbol);
if (!stockData) {
console.warn(`Failed to fetch data for ${item.symbol}`);
return item;
}
return {
company: stockData.company,
symbol: stockData.symbol,
currentPrice: stockData.currentPrice,
priceFormatted: stockData.priceFormatted,
changeFormatted: stockData.changeFormatted,
changePercent: stockData.changePercent,
marketCap: stockData.marketCapFormatted,
peRatio: stockData.peRatio,
};
}),
);
return JSON.parse(JSON.stringify(stocksWithData));
} catch (error) {
console.error('Error loading watchlist:', error);
throw new Error('Failed to fetch watchlist');
}
};Hint / Solution: Install Shadcn's Table UI and create the WatchlistTable
npx shadcn@latest add table'use client';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { WATCHLIST_TABLE_HEADER } from '@/lib/constants';
import { Button } from './ui/button';
import { WatchlistButton } from './WatchlistButton';
import { useRouter } from 'next/navigation';
import { cn, getChangeColorClass } from '@/lib/utils';
export function WatchlistTable({ watchlist }: WatchlistTableProps) {
const router = useRouter();
return (
<>
<Table className='scrollbar-hide-default watchlist-table'>
<TableHeader>
<TableRow className='table-header-row'>
{WATCHLIST_TABLE_HEADER.map((label) => (
<TableHead className='table-header' key={label}>
{label}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{watchlist.map((item, index) => (
<TableRow
key={item.symbol + index}
className='table-row'
onClick={() =>
router.push(`/stocks/${encodeURIComponent(item.symbol)}`)
}
>
<TableCell className='pl-4 table-cell'>{item.company}</TableCell>
<TableCell className='table-cell'>{item.symbol}</TableCell>
<TableCell className='table-cell'>
{item.priceFormatted || '—'}
</TableCell>
<TableCell
className={cn(
'table-cell',
getChangeColorClass(item.changePercent)
)}
>
{item.changeFormatted || '—'}
</TableCell>
<TableCell className='table-cell'>
{item.marketCap || '—'}
</TableCell>
<TableCell className='table-cell'>
{item.peRatio || '—'}
</TableCell>
<TableCell>
<Button className='add-alert'>Add Alert</Button>
</TableCell>
<TableCell>
<WatchlistButton
symbol={item.symbol}
company={item.company}
isInWatchlist={true}
showTrashIcon={true}
type='icon'
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
}Hint / Solution: IWatchlistTable in Watchlist page
import { Star } from 'lucide-react';
import { searchStocks } from '@/lib/actions/finnhub.actions';
import SearchCommand from '@/components/SearchCommand';
import { getWatchlistWithData } from '@/lib/actions/watchlist.actions';
import { WatchlistTable } from '@/components/WatchlistTable';
const Watchlist = async () => {
const watchlist = await getWatchlistWithData();
const initialStocks = await searchStocks();
// Empty state
if (watchlist.length === 0) {
return (
<section className="flex watchlist-empty-container">
<div className="watchlist-empty">
<Star className="watchlist-star" />
<h2 className="empty-title">Your watchlist is empty</h2>
<p className="empty-description">
Start building your watchlist by searching for stocks and clicking the star icon to add them.
</p>
</div>
<SearchCommand initialStocks={initialStocks} />
</section>
);
}
return (
<section className="watchlist">
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h2 className="watchlist-title">Watchlist</h2>
<SearchCommand initialStocks={initialStocks} />
</div>
<WatchlistTable watchlist={watchlist} />
</div>
</section>
);
};
export default Watchlist;After completing those steps, your app should allow a user to:
If any part doesn’t work, compare with the code hints above.
That’s it! Your watchlist is now fully functional — with a dedicated page and the ability to add or remove stocks from both search results and stock details. Great job 🎉