Add Hardware Price Tracker MVP

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 21:50:22 +00:00
parent 19b81e8912
commit 5fcbbcf200
8 changed files with 755 additions and 64 deletions

View File

@@ -3,12 +3,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>specseek-tracker</title> <title>Hardware Price Tracker - Real-time Monitoring & Alerts</title>
<meta name="description" content="Lovable Generated Project" /> <meta name="description" content="Track hardware prices across multiple stores with automated alerts and spec-based search profiles for SSDs, RAM, and more." />
<meta name="author" content="Lovable" /> <meta name="author" content="Hardware Price Tracker" />
<meta property="og:title" content="specseek-tracker" /> <meta property="og:title" content="Hardware Price Tracker - Smart Price Monitoring" />
<meta property="og:description" content="Lovable Generated Project" /> <meta property="og:description" content="Automated hardware price tracking with real-time alerts and spec-based searches." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" /> <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />

View File

@@ -0,0 +1,121 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { AlertCircle, ExternalLink } from "lucide-react";
interface AlertsListProps {
limit?: number;
}
export const AlertsList = ({ limit }: AlertsListProps) => {
// Mock data
const alerts = [
{
id: 1,
type: "product",
sourceTitle: "Samsung 990 PRO 2TB NVMe SSD",
message: "Price dropped to 699 DKK (target: 750 DKK)",
store: "proshop",
price: 699,
targetPrice: 750,
currency: "DKK",
url: "https://www.proshop.dk/example",
createdAt: "5 min ago",
},
{
id: 2,
type: "search_profile",
sourceTitle: "32GB DDR5-6000 RAM",
message: "Corsair Vengeance 32GB DDR5-6000 @ 899 DKK (target: 1000 DKK)",
store: "proshop",
price: 899,
targetPrice: 1000,
currency: "DKK",
url: "https://www.proshop.dk/example",
createdAt: "1 hour ago",
},
{
id: 3,
type: "search_profile",
sourceTitle: "1TB NVMe SSD Budget",
message: "Crucial P3 1TB @ 449 DKK (target: 500 DKK)",
store: "proshop",
price: 449,
targetPrice: 500,
currency: "DKK",
url: "https://www.proshop.dk/example",
createdAt: "3 hours ago",
},
{
id: 4,
type: "product",
sourceTitle: "WD Black SN850X 2TB NVMe",
message: "Price dropped to 1399 DKK (target: 1400 DKK)",
store: "proshop",
price: 1399,
targetPrice: 1400,
currency: "DKK",
url: "https://www.proshop.dk/example",
createdAt: "5 hours ago",
},
{
id: 5,
type: "product",
sourceTitle: "G.Skill Trident Z5 32GB DDR5-6400",
message: "Price dropped to 1099 DKK (target: 1200 DKK)",
store: "proshop",
price: 1099,
targetPrice: 1200,
currency: "DKK",
url: "https://www.proshop.dk/example",
createdAt: "8 hours ago",
},
];
const displayAlerts = limit ? alerts.slice(0, limit) : alerts;
const getDiscountPercent = (price: number, target: number) => {
return (((target - price) / target) * 100).toFixed(1);
};
return (
<div className="space-y-3">
{displayAlerts.map((alert) => (
<div
key={alert.id}
className="flex items-start gap-3 p-4 rounded-lg border border-border bg-destructive/5 hover:bg-destructive/10 transition-colors"
>
<div className="mt-0.5">
<AlertCircle className="h-5 w-5 text-destructive" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium truncate">{alert.sourceTitle}</h4>
<Badge variant="outline" className="text-xs capitalize">
{alert.type.replace("_", " ")}
</Badge>
</div>
<p className="text-sm text-foreground mb-2">{alert.message}</p>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="capitalize">{alert.store}</span>
<span></span>
<span>{alert.createdAt}</span>
<span></span>
<span className="text-success font-medium">
{getDiscountPercent(alert.price, alert.targetPrice)}% below target
</span>
</div>
</div>
<Button size="sm" variant="outline" asChild>
<a href={alert.url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
))}
</div>
);
};

View File

@@ -0,0 +1,73 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
export const PriceChart = () => {
// Mock data - will be replaced with real price history
const data = [
{ date: "Jan 1", samsung: 1499, wd: 1499, crucial: 599 },
{ date: "Jan 3", samsung: 1449, wd: 1450, crucial: 579 },
{ date: "Jan 5", samsung: 1399, wd: 1450, crucial: 569 },
{ date: "Jan 7", samsung: 1349, wd: 1399, crucial: 549 },
{ date: "Jan 9", samsung: 1299, wd: 1450, crucial: 549 },
];
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-card border border-border rounded-lg p-3 shadow-lg">
<p className="text-sm font-medium mb-2">{label}</p>
{payload.map((entry: any, index: number) => (
<p key={index} className="text-sm" style={{ color: entry.color }}>
{entry.name}: {entry.value} DKK
</p>
))}
</div>
);
}
return null;
};
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis
dataKey="date"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
/>
<Tooltip content={<CustomTooltip />} />
<Legend
wrapperStyle={{ fontSize: '12px' }}
/>
<Line
type="monotone"
dataKey="samsung"
stroke="hsl(var(--chart-1))"
name="Samsung 990 PRO"
strokeWidth={2}
dot={{ fill: "hsl(var(--chart-1))" }}
/>
<Line
type="monotone"
dataKey="wd"
stroke="hsl(var(--chart-2))"
name="WD Black SN850X"
strokeWidth={2}
dot={{ fill: "hsl(var(--chart-2))" }}
/>
<Line
type="monotone"
dataKey="crucial"
stroke="hsl(var(--chart-3))"
name="Crucial P5 Plus"
strokeWidth={2}
dot={{ fill: "hsl(var(--chart-3))" }}
/>
</LineChart>
</ResponsiveContainer>
);
};

View File

@@ -0,0 +1,151 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ExternalLink, TrendingDown, TrendingUp } from "lucide-react";
interface ProductsListProps {
limit?: number;
sortBy?: "date" | "deal";
}
export const ProductsList = ({ limit, sortBy = "date" }: ProductsListProps) => {
// Mock data - will be replaced with real data from Lovable Cloud
const products = [
{
id: 1,
name: "Samsung 990 PRO 2TB NVMe SSD",
store: "proshop",
currentPrice: 1299,
targetPrice: 1500,
lastPrice: 1399,
currency: "DKK",
url: "https://www.proshop.dk/example",
lastChecked: "2 min ago",
availability: "in_stock",
},
{
id: 2,
name: "Corsair Vengeance RGB 32GB DDR5-6000",
store: "proshop",
currentPrice: 899,
targetPrice: 1000,
lastPrice: 949,
currency: "DKK",
url: "https://www.proshop.dk/example",
lastChecked: "5 min ago",
availability: "in_stock",
},
{
id: 3,
name: "WD Black SN850X 2TB NVMe",
store: "proshop",
currentPrice: 1450,
targetPrice: 1400,
lastPrice: 1399,
currency: "DKK",
url: "https://www.proshop.dk/example",
lastChecked: "8 min ago",
availability: "in_stock",
},
{
id: 4,
name: "G.Skill Trident Z5 32GB DDR5-6400",
store: "proshop",
currentPrice: 1099,
targetPrice: 1200,
lastPrice: 1149,
currency: "DKK",
url: "https://www.proshop.dk/example",
lastChecked: "12 min ago",
availability: "in_stock",
},
{
id: 5,
name: "Crucial P5 Plus 1TB NVMe",
store: "proshop",
currentPrice: 549,
targetPrice: 600,
lastPrice: 599,
currency: "DKK",
url: "https://www.proshop.dk/example",
lastChecked: "15 min ago",
availability: "in_stock",
},
];
const displayProducts = limit ? products.slice(0, limit) : products;
const getPriceChange = (current: number, last: number) => {
const change = ((current - last) / last) * 100;
return change.toFixed(1);
};
const isPriceDown = (current: number, last: number) => current < last;
const isBelowTarget = (current: number, target: number) => current <= target;
return (
<div className="space-y-3">
{displayProducts.map((product) => {
const priceChange = getPriceChange(product.currentPrice, product.lastPrice);
const priceDown = isPriceDown(product.currentPrice, product.lastPrice);
const belowTarget = isBelowTarget(product.currentPrice, product.targetPrice);
return (
<div
key={product.id}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-secondary/30 hover:bg-secondary/50 transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium truncate">{product.name}</h4>
{belowTarget && (
<Badge variant="default" className="bg-success text-success-foreground">
Target Hit
</Badge>
)}
{product.availability === "in_stock" && (
<Badge variant="outline" className="text-xs">
In Stock
</Badge>
)}
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<span className="capitalize">{product.store}</span>
<span></span>
<span>Target: {product.targetPrice} {product.currency}</span>
<span></span>
<span>{product.lastChecked}</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="flex items-center gap-2">
<span className="text-2xl font-bold">
{product.currentPrice} <span className="text-sm text-muted-foreground">{product.currency}</span>
</span>
</div>
<div className="flex items-center gap-1 text-sm">
{priceDown ? (
<TrendingDown className="h-4 w-4 text-success" />
) : (
<TrendingUp className="h-4 w-4 text-destructive" />
)}
<span className={priceDown ? "text-success" : "text-destructive"}>
{priceDown ? "" : "+"}{priceChange}%
</span>
</div>
</div>
<Button size="sm" variant="outline" asChild>
<a href={product.url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,162 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Search, TrendingDown } from "lucide-react";
export const SearchProfiles = () => {
// Mock data
const profiles = [
{
id: 1,
title: "2TB NVMe SSD",
category: "ssd",
filters: {
capacity_gb: 2000,
interface: "NVMe",
brand: ["Samsung", "Crucial"],
},
targetPrice: 750,
currency: "DKK",
bestMatch: {
name: "Samsung 990 PRO 2TB",
price: 699,
store: "proshop",
},
active: true,
},
{
id: 2,
title: "32GB DDR5-6000 RAM",
category: "ram",
filters: {
capacity_gb: 32,
type: "DDR5",
brand: ["Corsair", "G.Skill"],
},
targetPrice: 1000,
currency: "DKK",
bestMatch: {
name: "Corsair Vengeance 32GB DDR5-6000",
price: 899,
store: "proshop",
},
active: true,
},
{
id: 3,
title: "1TB NVMe SSD Budget",
category: "ssd",
filters: {
capacity_gb: 1000,
interface: "NVMe",
},
targetPrice: 500,
currency: "DKK",
bestMatch: {
name: "Crucial P3 1TB",
price: 449,
store: "proshop",
},
active: true,
},
{
id: 4,
title: "16GB DDR4-3200 RAM",
category: "ram",
filters: {
capacity_gb: 16,
type: "DDR4",
},
targetPrice: 400,
currency: "DKK",
bestMatch: null,
active: true,
},
];
const getFilterSummary = (filters: any) => {
const parts = [];
if (filters.capacity_gb) {
parts.push(filters.capacity_gb >= 1000 ? `${filters.capacity_gb / 1000}TB` : `${filters.capacity_gb}GB`);
}
if (filters.interface) {
parts.push(filters.interface);
}
if (filters.type) {
parts.push(filters.type);
}
if (filters.brand && filters.brand.length > 0) {
parts.push(`${filters.brand.join(", ")}`);
}
return parts.join(" • ");
};
return (
<div className="space-y-3">
{profiles.map((profile) => {
const belowTarget = profile.bestMatch && profile.bestMatch.price <= profile.targetPrice;
return (
<div
key={profile.id}
className="p-4 rounded-lg border border-border bg-secondary/30 hover:bg-secondary/50 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Search className="h-4 w-4 text-primary" />
<h4 className="font-medium">{profile.title}</h4>
{belowTarget && (
<Badge variant="default" className="bg-success text-success-foreground">
<TrendingDown className="h-3 w-3 mr-1" />
Deal Found
</Badge>
)}
{profile.active && (
<Badge variant="outline" className="text-xs">Active</Badge>
)}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge variant="secondary" className="text-xs uppercase">{profile.category}</Badge>
<span></span>
<span>{getFilterSummary(profile.filters)}</span>
<span></span>
<span>Target: {profile.targetPrice} {profile.currency}</span>
</div>
</div>
<Button size="sm" variant="ghost">
Edit
</Button>
</div>
{profile.bestMatch ? (
<div className="pl-6 pt-3 border-t border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">{profile.bestMatch.name}</p>
<p className="text-xs text-muted-foreground capitalize">
{profile.bestMatch.store}
</p>
</div>
<div className="text-right">
<div className="text-lg font-bold">
{profile.bestMatch.price} <span className="text-sm text-muted-foreground">{profile.currency}</span>
</div>
{belowTarget && (
<p className="text-xs text-success">
{((profile.targetPrice - profile.bestMatch.price) / profile.targetPrice * 100).toFixed(1)}% below target
</p>
)}
</div>
</div>
</div>
) : (
<div className="pl-6 pt-3 border-t border-border">
<p className="text-sm text-muted-foreground">No matches found yet</p>
</div>
)}
</div>
);
})}
</div>
);
};

View File

@@ -8,89 +8,95 @@ All colors MUST be HSL.
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 220 26% 14%;
--foreground: 222.2 84% 4.9%; --foreground: 210 40% 98%;
--card: 0 0% 100%; --card: 220 24% 18%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 210 40% 98%;
--popover: 0 0% 100%; --popover: 220 24% 18%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%;
--primary: 222.2 47.4% 11.2%; --primary: 217 91% 60%;
--primary-foreground: 210 40% 98%; --primary-foreground: 222 47% 11%;
--secondary: 210 40% 96.1%; --secondary: 220 17% 25%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 210 40% 98%;
--muted: 210 40% 96.1%; --muted: 220 17% 25%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215 20% 65%;
--accent: 210 40% 96.1%; --accent: 263 70% 60%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--success: 142 76% 36%;
--success-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%; --border: 220 17% 28%;
--input: 214.3 31.8% 91.4%; --input: 220 17% 28%;
--ring: 222.2 84% 4.9%; --ring: 217 91% 60%;
--radius: 0.5rem; --radius: 0.75rem;
--sidebar-background: 0 0% 98%; --chart-1: 217 91% 60%;
--chart-2: 263 70% 60%;
--chart-3: 142 76% 36%;
--chart-4: 43 96% 56%;
--chart-5: 0 84% 60%;
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-background: 220 26% 14%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 240 5.9% 10%; --sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 222 47% 11%;
--sidebar-primary-foreground: 0 0% 98%; --sidebar-accent: 220 17% 25%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-accent: 240 4.8% 95.9%; --sidebar-border: 220 17% 28%;
--sidebar-ring: 217 91% 60%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 220 26% 14%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
--card: 222.2 84% 4.9%; --card: 220 24% 18%;
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%; --popover: 220 24% 18%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 210 40% 98%; --primary: 217 91% 60%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 222 47% 11%;
--secondary: 217.2 32.6% 17.5%; --secondary: 220 17% 25%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%; --muted: 220 17% 25%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20% 65%;
--accent: 217.2 32.6% 17.5%; --accent: 263 70% 60%;
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--success: 142 76% 36%;
--success-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 220 17% 28%;
--input: 217.2 32.6% 17.5%; --input: 220 17% 28%;
--ring: 212.7 26.8% 83.9%; --ring: 217 91% 60%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%; --sidebar-background: 220 26% 14%;
--sidebar-primary: 224.3 76.3% 48%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary: 217 91% 60%;
--sidebar-accent: 240 3.7% 15.9%; --sidebar-primary-foreground: 222 47% 11%;
--sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-accent: 220 17% 25%;
--sidebar-border: 240 3.7% 15.9%; --sidebar-accent-foreground: 210 40% 98%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-border: 220 17% 28%;
--sidebar-ring: 217 91% 60%;
} }
} }

View File

@@ -1,11 +1,178 @@
// Update this page (the content is just a fallback if you fail to update the page) import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { AlertCircle, TrendingDown, TrendingUp, Activity, Plus, Search, Package } from "lucide-react";
import { ProductsList } from "@/components/ProductsList";
import { SearchProfiles } from "@/components/SearchProfiles";
import { AlertsList } from "@/components/AlertsList";
import { PriceChart } from "@/components/PriceChart";
const Index = () => { const Index = () => {
const [activeTab, setActiveTab] = useState("dashboard");
// Mock data - will be replaced with real data later
const stats = {
trackedProducts: 12,
activeProfiles: 5,
recentAlerts: 3,
avgPriceDrop: 8.5,
};
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="min-h-screen bg-background">
<div className="text-center"> {/* Header */}
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1> <header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-50">
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p> <div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Activity className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Hardware Price Tracker</h1>
<p className="text-sm text-muted-foreground">Real-time monitoring & alerts</p>
</div>
</div>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Add Product
</Button>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
{/* Stats Overview */}
<div className="grid gap-4 md:grid-cols-4 mb-8">
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Tracked Products</CardTitle>
<Package className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.trackedProducts}</div>
<p className="text-xs text-muted-foreground mt-1">Active monitoring</p>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Search Profiles</CardTitle>
<Search className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.activeProfiles}</div>
<p className="text-xs text-muted-foreground mt-1">Spec-based searches</p>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Recent Alerts</CardTitle>
<AlertCircle className="h-4 w-4 text-destructive" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.recentAlerts}</div>
<p className="text-xs text-muted-foreground mt-1">Last 24 hours</p>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Avg Price Drop</CardTitle>
<TrendingDown className="h-4 w-4 text-success" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.avgPriceDrop}%</div>
<p className="text-xs text-muted-foreground mt-1">Below target</p>
</CardContent>
</Card>
</div>
{/* Main Content Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="bg-card/50 border border-border">
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="products">Products</TabsTrigger>
<TabsTrigger value="profiles">Search Profiles</TabsTrigger>
<TabsTrigger value="alerts">Alerts</TabsTrigger>
</TabsList>
<TabsContent value="dashboard" className="space-y-6">
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle>Price Trends</CardTitle>
<CardDescription>Latest price movements for tracked products</CardDescription>
</CardHeader>
<CardContent>
<PriceChart />
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
Recent Alerts
</CardTitle>
<CardDescription>Price drops below target threshold</CardDescription>
</CardHeader>
<CardContent>
<AlertsList limit={5} />
</CardContent>
</Card>
</div>
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle>Top Opportunities</CardTitle>
<CardDescription>Best deals currently available</CardDescription>
</CardHeader>
<CardContent>
<ProductsList limit={5} sortBy="deal" />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="products">
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle>Tracked Products</CardTitle>
<CardDescription>Direct URL monitoring for specific products</CardDescription>
</CardHeader>
<CardContent>
<ProductsList />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="profiles">
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle>Search Profiles</CardTitle>
<CardDescription>Spec-based searches with brand filters</CardDescription>
</CardHeader>
<CardContent>
<SearchProfiles />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="alerts">
<Card className="bg-card/50 backdrop-blur-sm border-border">
<CardHeader>
<CardTitle>Alert History</CardTitle>
<CardDescription>All price drop notifications</CardDescription>
</CardHeader>
<CardContent>
<AlertsList />
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div> </div>
</div> </div>
); );

View File

@@ -31,6 +31,10 @@ export default {
DEFAULT: "hsl(var(--destructive))", DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))", foreground: "hsl(var(--destructive-foreground))",
}, },
success: {
DEFAULT: "hsl(var(--success))",
foreground: "hsl(var(--success-foreground))",
},
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))", foreground: "hsl(var(--muted-foreground))",
@@ -57,6 +61,13 @@ export default {
border: "hsl(var(--sidebar-border))", border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))", ring: "hsl(var(--sidebar-ring))",
}, },
chart: {
"1": "hsl(var(--chart-1))",
"2": "hsl(var(--chart-2))",
"3": "hsl(var(--chart-3))",
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",