Skip to content
Closed
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
3 changes: 3 additions & 0 deletions ecomm-demo/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Apify API Token
# Get your token from: https://console.apify.com/account#/integrations
APIFY_TOKEN=your_apify_token_here
7 changes: 4 additions & 3 deletions ecomm-demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
.env
.env.local
.env.*.local

# vercel
.vercel
Expand All @@ -42,5 +44,4 @@ next-env.d.ts

# internal
task-manager.md
PRD.md
README.md
PRD.md
129 changes: 129 additions & 0 deletions ecomm-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# E-Commerce Demo with Apify Integration

A Next.js application that demonstrates integration with the Apify E-Commerce Scraper to search and display real product data from e-commerce websites.

## Features

- 🔍 Real-time product search using Apify E-Commerce Scraper
- 📊 Dynamic statistics including product count and average price
- 📋 Product data displayed in table and card formats
- ⚡ Loading states and error handling
- 🎨 Built with Next.js, TypeScript, Tailwind CSS, and shadcn/ui

## Prerequisites

- Node.js 20.x or higher
- An Apify account with an API token

## Setup

### 1. Install Dependencies

```bash
npm install
```

### 2. Configure Apify Token

1. Create a free Apify account at [https://console.apify.com](https://console.apify.com)
2. Get your API token from [https://console.apify.com/account#/integrations](https://console.apify.com/account#/integrations)
3. Create a `.env.local` file in the root directory:

```bash
cp .env.example .env.local
```

4. Add your Apify token to `.env.local`:

```
NEXT_PUBLIC_APIFY_TOKEN=your_apify_token_here
```

⚠️ **Important**: The token must be prefixed with `NEXT_PUBLIC_` to be accessible in the browser.

### 3. Run the Development Server

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser.

## Usage

1. Enter a product keyword in the search bar (e.g., "laptop", "headphones", "camera")
2. Click "Submit" to search for products
3. The app will use Apify E-Commerce Scraper to fetch real product data from Amazon
4. Results will be displayed in both table and card formats
5. Statistics cards show the total number of products and average price

## How It Works

The application uses the Apify E-Commerce Scraping Tool (`apify/e-commerce-scraping-tool`) to:

1. Search for products by keyword on Amazon (default marketplace)
2. Extract product information including:
- Product name
- Price
- Image URL
- Description
- Product URL
3. Display the results in a user-friendly interface

## Project Structure

```
ecomm-demo/
├── app/
│ ├── page.tsx # Main page component with search logic
│ ├── layout.tsx # Root layout
│ └── globals.css # Global styles
├── components/
│ ├── SearchBar.tsx # Search input component
│ ├── StatsCards.tsx # Statistics display
│ ├── ProductTable.tsx # Product data table
│ ├── ProductCards.tsx # Product card grid
│ └── ui/ # shadcn/ui components
├── lib/
│ ├── apify-service.ts # Apify integration service
│ └── types.ts # TypeScript type definitions
├── data/
│ └── products.ts # Mock data (used as fallback)
└── .env.example # Environment variable template
```

## Available Scripts

- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run start` - Start production server
- `npm run lint` - Run ESLint

## Pricing

The E-Commerce Scraping Tool uses a pay-per-event model. The $5 credit included in the Apify free plan lets you scrape approximately 800 product URLs. For more details, see the [Actor's pricing page](https://apify.com/apify/e-commerce-scraping-tool/pricing).

## Troubleshooting

### "Failed to search products" Error

- **Check your API token**: Make sure it's correctly set in `.env.local`
- **Verify token prefix**: The environment variable must start with `NEXT_PUBLIC_`
- **Restart the dev server**: After changing `.env.local`, restart `npm run dev`

### No Results Found

- Try different search keywords
- Check that your Apify account has sufficient credits
- Review the browser console for detailed error messages

## Learn More

- [Apify Documentation](https://docs.apify.com)
- [E-Commerce Scraping Tool](https://apify.com/apify/e-commerce-scraping-tool)
- [Next.js Documentation](https://nextjs.org/docs)
- [Apify JavaScript Client](https://docs.apify.com/api/client/js)

## License

This project is provided as a demonstration of Apify integration.
68 changes: 63 additions & 5 deletions ecomm-demo/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
import { products } from "@/data/products";
"use client";

import { useState } from "react";
import { products as mockProducts } from "@/data/products";
import { StatsCards } from "@/components/StatsCards";
import { ProductTable } from "@/components/ProductTable";
import { ProductCards } from "@/components/ProductCards";
import { SearchBar } from "@/components/SearchBar";
import { Separator } from "@/components/ui/separator";
import { searchProducts } from "@/lib/apify-service";
import { Product } from "@/lib/types";
import Image from "next/image";

export default function Home() {
const [products, setProducts] = useState<Product[]>(mockProducts);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [dataSource, setDataSource] = useState<"Mock" | "Apify">("Mock");

const handleSearch = async (query: string) => {
if (!query.trim()) {
setError("Please enter a search query");
return;
}

setIsLoading(true);
setError(null);

try {
const results = await searchProducts({ keyword: query });

if (results.length === 0) {
setError(`No products found for "${query}". Try a different search term.`);
setProducts([]);
} else {
setProducts(results);
setDataSource("Apify");
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "An unknown error occurred";
setError(`Failed to search products: ${errorMessage}`);
console.error("Search error:", err);
} finally {
setIsLoading(false);
}
};

return (
<div className="min-h-screen bg-background">
{/* Header */}
Expand Down Expand Up @@ -37,19 +75,39 @@ export default function Home() {
Product Catalog Demo
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mb-6">
Showcase of e-commerce product data ready for Apify integration.
This demo displays scraped product information in multiple formats.
Search for products using Apify E-Commerce Scraper.
Enter a keyword to scrape real product data from Amazon.
</p>

{/* Search Bar */}
<div className="mt-8">
<SearchBar />
<SearchBar onSearch={handleSearch} disabled={isLoading} />
</div>

{/* Loading State */}
{isLoading && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-blue-800">
🔄 Searching for products... This may take a moment.
</p>
</div>
)}

{/* Error State */}
{error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800">❌ {error}</p>
</div>
)}
</div>

{/* Stats Section */}
<section className="mb-12">
<StatsCards productCount={products.length} />
<StatsCards
productCount={products.length}
products={products}
dataSource={dataSource}
/>
</section>

<Separator className="my-12" />
Expand Down
12 changes: 6 additions & 6 deletions ecomm-demo/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@ import { Search } from "lucide-react";

interface SearchBarProps {
onSearch?: (query: string) => void;
disabled?: boolean;
}

export function SearchBar({ onSearch }: SearchBarProps) {
export function SearchBar({ onSearch, disabled = false }: SearchBarProps) {
const [query, setQuery] = useState("");

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSearch) {
if (onSearch && !disabled) {
onSearch(query);
}
// Placeholder for actual search functionality
console.log("Search query:", query);
};

return (
Expand All @@ -32,10 +31,11 @@ export function SearchBar({ onSearch }: SearchBarProps) {
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-10"
disabled={disabled}
/>
</div>
<Button type="submit" className="px-8">
Submit
<Button type="submit" className="px-8" disabled={disabled}>
{disabled ? "Searching..." : "Submit"}
</Button>
</div>
</form>
Expand Down
28 changes: 22 additions & 6 deletions ecomm-demo/components/StatsCards.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Package } from "lucide-react";
import { Product } from "@/lib/types";

interface StatsCardsProps {
productCount: number;
products?: Product[];
dataSource?: "Mock" | "Apify";
}

export function StatsCards({ productCount }: StatsCardsProps) {
export function StatsCards({ productCount, products = [], dataSource = "Mock" }: StatsCardsProps) {
// Calculate average price from products
const calculateAveragePrice = () => {
if (products.length === 0) return 0;
const validPrices = products.filter(p => p.price > 0);
if (validPrices.length === 0) return 0;
const sum = validPrices.reduce((acc, p) => acc + p.price, 0);
return sum / validPrices.length;
};

const averagePrice = calculateAveragePrice();

return (
<div className="grid gap-4 md:grid-cols-3">
<Card>
Expand All @@ -27,9 +41,11 @@ export function StatsCards({ productCount }: StatsCardsProps) {
<span className="text-lg">$</span>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">-</div>
<div className="text-2xl font-bold">
{averagePrice > 0 ? `$${averagePrice.toFixed(2)}` : "-"}
</div>
<p className="text-xs text-muted-foreground">
Will be calculated from data
{averagePrice > 0 ? "Calculated from scraped data" : "No price data available"}
</p>
</CardContent>
</Card>
Expand All @@ -40,9 +56,9 @@ export function StatsCards({ productCount }: StatsCardsProps) {
<span className="text-lg">🔗</span>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">Mock</div>
<div className="text-2xl font-bold">{dataSource}</div>
<p className="text-xs text-muted-foreground">
Ready for Apify integration
{dataSource === "Apify" ? "Live scraped data" : "Ready for Apify integration"}
</p>
</CardContent>
</Card>
Expand Down
Loading
Loading