Stock Widgets

PreviousNext

Stock market widgets for displaying stock prices, trends, and news with liquid glass styling.

Docs
einuicomponent

Preview

Loading preview…
registry/widgets/stock-widget.tsx
"use client";
import { cn } from "@/lib/utils";
import { TrendingUp, TrendingDown, ArrowUpRight, ArrowDownRight } from "lucide-react";
import { GlassWidgetBase } from "./base-widget";

interface StockTickerWidgetProps {
  symbol: string;
  name?: string;
  price: number;
  change: number;
  changePercent: number;
  chartData?: number[];
  className?: string;
}

function StockTickerWidget({
  symbol,
  name,
  price,
  change,
  changePercent,
  chartData,
  className,
}: StockTickerWidgetProps) {
  const isPositive = change >= 0;
  const color = isPositive ? "text-emerald-500" : "text-red-500";

  return (
    <GlassWidgetBase className={cn("min-w-45", className)} glowColor={isPositive ? "green" : "red"}>
      <div className="flex items-start justify-between mb-2">
        <div className="flex items-center gap-2">
          {isPositive ? (
            <TrendingUp className="w-4 h-4 text-emerald-500" />
          ) : (
            <TrendingDown className="w-4 h-4 text-red-500" />
          )}
          <span className="text-white font-medium">{symbol}</span>
        </div>
        <span className={cn("text-sm tabular-nums", color)}>
          {isPositive ? "+" : ""}
          {change.toFixed(2)}
        </span>
      </div>

      {chartData && chartData.length > 0 && (
        <div className="h-12 flex items-end gap-px my-3">
          {chartData.map((value, i) => {
            const max = Math.max(...chartData);
            const min = Math.min(...chartData);
            const height = ((value - min) / (max - min || 1)) * 100;
            return (
              <div
                key={i}
                className={cn(
                  "flex-1 rounded-t transition-all",
                  isPositive ? "bg-emerald-500/60" : "bg-red-500/60"
                )}
                style={{ height: `${Math.max(height, 10)}%` }}
              />
            );
          })}
        </div>
      )}

      <div className="flex items-end justify-between">
        <div>
          <div className="text-2xl font-light text-white tabular-nums">{price.toFixed(2)}</div>
          {name && <div className="text-sm text-white/50 truncate max-w-30">{name}</div>}
        </div>
        <span className={cn("text-sm tabular-nums", color)}>
          {isPositive ? "+" : ""}
          {changePercent.toFixed(2)}%
        </span>
      </div>
    </GlassWidgetBase>
  );
}

interface CompactStockWidgetProps {
  symbol: string;
  price: number;
  change: number;
  changePercent: number;
  className?: string;
}

function CompactStockWidget({
  symbol,
  price,
  change,
  changePercent,
  className,
}: CompactStockWidgetProps) {
  const isPositive = change >= 0;

  return (
    <GlassWidgetBase
      className={cn("min-w-35", className)}
      size="sm"
      glowColor={isPositive ? "green" : "red"}
    >
      <div className="flex items-center justify-between">
        <span className="text-white font-medium">{symbol}</span>
        <span
          className={cn(
            "flex items-center gap-0.5 text-sm",
            isPositive ? "text-emerald-500" : "text-red-500"
          )}
        >
          {isPositive ? (
            <ArrowUpRight className="w-3 h-3" />
          ) : (
            <ArrowDownRight className="w-3 h-3" />
          )}
          {Math.abs(changePercent).toFixed(2)}%
        </span>
      </div>
      <div className="text-xl font-light text-white mt-1 tabular-nums">${price.toFixed(2)}</div>
    </GlassWidgetBase>
  );
}

interface PortfolioItem {
  symbol: string;
  name: string;
  shares: number;
  avgCost: number;
  currentPrice: number;
}

interface PortfolioWidgetProps {
  title?: string;
  totalValue?: number;
  totalChange?: number;
  holdings?: PortfolioItem[];
  className?: string;
}

function PortfolioWidget({
  title = "Portfolio",
  totalValue = 0,
  totalChange = 0,
  holdings = [],
  className,
}: PortfolioWidgetProps) {
  const isPositive = totalChange >= 0;

  return (
    <GlassWidgetBase
      className={cn("min-w-70", className)}
      size="lg"
      glowColor={isPositive ? "green" : "red"}
    >
      <div className="flex items-start justify-between mb-4">
        <div>
          <h3 className="text-white/60 text-sm">{title}</h3>
          <div className="text-2xl font-light text-white tabular-nums">
            ${totalValue.toLocaleString()}
          </div>
        </div>
        <span
          className={cn(
            "flex items-center gap-1 text-sm",
            isPositive ? "text-emerald-500" : "text-red-500"
          )}
        >
          {isPositive ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
          {isPositive ? "+" : ""}
          {totalChange.toFixed(2)}%
        </span>
      </div>

      <div className="space-y-2">
        {holdings.map((item) => {
          const value = item.shares * item.currentPrice;
          const gain = ((item.currentPrice - item.avgCost) / item.avgCost) * 100;
          const isGain = gain >= 0;

          return (
            <div
              key={item.symbol}
              className="flex items-center justify-between p-2 rounded-lg bg-white/5 border border-white/5"
            >
              <div>
                <div className="text-white font-medium">{item.symbol}</div>
                <div className="text-white/50 text-xs">{item.shares} shares</div>
              </div>
              <div className="text-right">
                <div className="text-white tabular-nums">${value.toFixed(2)}</div>
                <div
                  className={cn(
                    "text-xs tabular-nums",
                    isGain ? "text-emerald-500" : "text-red-500"
                  )}
                >
                  {isGain ? "+" : ""}
                  {gain.toFixed(2)}%
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </GlassWidgetBase>
  );
}

interface MarketIndex {
  name: string;
  value: number;
  change: number;
  changePercent: number;
}

interface MarketOverviewWidgetProps {
  indices?: MarketIndex[];
  className?: string;
}

function MarketOverviewWidget({ indices = [], className }: MarketOverviewWidgetProps) {
  return (
    <GlassWidgetBase className={cn("min-w-60", className)} glowColor="cyan">
      <h3 className="text-white/60 text-sm mb-4">Market Overview</h3>
      <div className="space-y-3">
        {indices.map((index) => {
          const isPositive = index.change >= 0;
          return (
            <div key={index.name} className="flex items-center justify-between">
              <span className="text-white/80">{index.name}</span>
              <div className="flex items-center gap-3">
                <span className="text-white tabular-nums">{index.value.toLocaleString()}</span>
                <span
                  className={cn(
                    "flex items-center gap-0.5 text-sm tabular-nums",
                    isPositive ? "text-emerald-500" : "text-red-500"
                  )}
                >
                  {isPositive ? (
                    <ArrowUpRight className="w-3 h-3" />
                  ) : (
                    <ArrowDownRight className="w-3 h-3" />
                  )}
                  {Math.abs(index.changePercent).toFixed(2)}%
                </span>
              </div>
            </div>
          );
        })}
      </div>
    </GlassWidgetBase>
  );
}

interface CryptoWidgetProps {
  symbol: string;
  name: string;
  price: number;
  change24h: number;
  marketCap?: string;
  volume24h?: string;
  sparkline?: number[];
  className?: string;
}

function CryptoWidget({
  symbol,
  name,
  price,
  change24h,
  marketCap,
  volume24h,
  sparkline,
  className,
}: CryptoWidgetProps) {
  const isPositive = change24h >= 0;

  return (
    <GlassWidgetBase className={cn("min-w-50", className)} glowColor={isPositive ? "green" : "red"}>
      <div className="flex items-start justify-between mb-3">
        <div>
          <div className="text-white font-medium">{symbol}</div>
          <div className="text-white/50 text-sm">{name}</div>
        </div>
        <span
          className={cn("text-sm tabular-nums", isPositive ? "text-emerald-500" : "text-red-500")}
        >
          {isPositive ? "+" : ""}
          {change24h.toFixed(2)}%
        </span>
      </div>

      {sparkline && sparkline.length > 0 && (
        <div className="h-10 flex items-end gap-px mb-3">
          {sparkline.map((value, i) => {
            const max = Math.max(...sparkline);
            const min = Math.min(...sparkline);
            const height = ((value - min) / (max - min || 1)) * 100;
            return (
              <div
                key={i}
                className={cn(
                  "flex-1 rounded-t",
                  isPositive ? "bg-emerald-500/50" : "bg-red-500/50"
                )}
                style={{ height: `${Math.max(height, 5)}%` }}
              />
            );
          })}
        </div>
      )}

      <div className="text-2xl font-light text-white mb-2 tabular-nums">
        ${price.toLocaleString()}
      </div>

      {(marketCap || volume24h) && (
        <div className="flex items-center gap-4 text-xs text-white/50">
          {marketCap && <span>MCap: {marketCap}</span>}
          {volume24h && <span>Vol: {volume24h}</span>}
        </div>
      )}
    </GlassWidgetBase>
  );
}

export {
  StockTickerWidget,
  CompactStockWidget,
  PortfolioWidget,
  MarketOverviewWidget,
  CryptoWidget,
};

Installation

npx shadcn@latest add @einui/stock-widget

Usage

import { StockWidget } from "@/components/stock-widget"
<StockWidget />