Skip to content
Draft
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 social-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
169 changes: 169 additions & 0 deletions social-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Instagram Analytics Demo

A Next.js demo application that analyzes Instagram profiles using the Apify Instagram Profile Scraper.

## Features

- 📊 Real-time Instagram profile analysis
- 📈 Engagement metrics and charts
- #️⃣ Top hashtags visualization
- 📸 Recent posts grid
- 🎨 Modern UI with Tailwind CSS and shadcn/ui

## Setup

### Prerequisites

- Node.js 18+ installed
- An Apify account with an API token

### 1. Get Your Apify Token

1. Sign up for a free account at [https://console.apify.com](https://console.apify.com)
2. Navigate to **Settings → Integrations**
3. Copy your API token

### 2. Install Dependencies

```bash
npm install
```

### 3. Configure Environment Variables

Create a `.env.local` file in the root directory:

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

Edit `.env.local` and add your Apify token:

```
APIFY_TOKEN=your_actual_apify_token_here
```

**Important:** Never commit your `.env.local` file to version control.

### 4. Run the Development Server

```bash
npm run dev
```

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

## Usage

1. Enter an Instagram username or profile URL in the search bar
2. Click "Analyze Profile"
3. View detailed analytics including:
- Profile information (followers, following, posts count)
- Engagement metrics
- Recent posts with engagement rates
- Top hashtags
- Engagement history chart

## How It Works

### Architecture

```
User Input → SearchBar → API Route → Apify Actor → Transform → Display
```

1. **User enters a username** in the search bar
2. **Next.js API route** (`/api/instagram/[username]`) receives the request
3. **Apify Client** calls the Instagram Profile Scraper Actor
4. **Data transformation** maps Apify's output to our TypeScript types
5. **UI components** display the transformed data

### Key Files

- `app/page.tsx` - Main page component with search handling
- `app/api/instagram/[username]/route.ts` - API route that calls Apify
- `lib/apify-transform.ts` - Transforms Apify data to our format
- `lib/types.ts` - TypeScript interfaces for Instagram data
- `components/` - Reusable UI components

## Apify Integration

This app uses the [Instagram Profile Scraper](https://apify.com/apify/instagram-profile-scraper) Actor, which:

- Extracts public profile data (no private accounts)
- Returns profile info, metrics, and recent posts
- Uses a pay-per-result pricing model
- Includes ~2,000 free results with the $5 starter credit

### Cost Considerations

- Free tier: $2.60 per 1,000 profiles (~2,000 free profiles)
- Paid plans: Discounted rates starting at $2.30/1,000

## Development

### Build

```bash
npm run build
```

### Lint

```bash
npm run lint
```

## Tech Stack

- **Framework:** Next.js 16 (App Router)
- **Language:** TypeScript
- **Styling:** Tailwind CSS
- **UI Components:** shadcn/ui
- **Charts:** Recharts
- **Data Scraping:** Apify (apify-client)

## Limitations

- Only works with **public** Instagram profiles
- Private accounts will return an error
- Rate limits apply based on your Apify plan
- Historical engagement data is limited to recent posts

## Learn More

- [Apify Documentation](https://docs.apify.com)
- [Instagram Profile Scraper](https://apify.com/apify/instagram-profile-scraper)
- [Next.js Documentation](https://nextjs.org/docs)
- [Apify JavaScript SDK](https://docs.apify.com/sdk/js/)

## Troubleshooting

### "APIFY_TOKEN not set" error

Make sure you've created `.env.local` with your token:

```bash
APIFY_TOKEN=your_token_here
```

Restart the dev server after adding the token.

### "Profile not found" error

- Verify the username is correct
- Check if the account is public (private accounts cannot be scraped)
- Try using just the username without @ or URL

### Actor timeout

If the Actor takes too long, it might be due to:
- Instagram rate limiting
- Network issues
- Heavy traffic on Apify

Try again after a few minutes.

## License

This is a demo application. Check individual package licenses for production use.
86 changes: 86 additions & 0 deletions social-demo/app/api/instagram/[username]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { NextRequest, NextResponse } from "next/server";
import { ApifyClient } from "apify-client";
import { transformApifyToProfile } from "@/lib/apify-transform";

// Initialize the Apify client
const getApifyClient = () => {
const token = process.env.APIFY_TOKEN;

if (!token) {
throw new Error("APIFY_TOKEN environment variable is not set");
}

return new ApifyClient({ token });
};

export async function GET(
request: NextRequest,
context: { params: Promise<{ username: string }> }
) {
try {
const { username } = await context.params;

if (!username) {
return NextResponse.json(
{ error: "Username is required" },
{ status: 400 }
);
}

// Clean the username (remove @ if present)
const cleanUsername = username.replace("@", "").trim();

console.log(`Fetching Instagram profile for: ${cleanUsername}`);

// Initialize Apify client
const client = getApifyClient();

// Call the Instagram Profile Scraper Actor
const run = await client.actor("apify/instagram-profile-scraper").call({
usernames: [cleanUsername],
includeAboutSection: false, // Set to true if you have a paid plan
});

// Get the results from the dataset
const { items } = await client.dataset(run.defaultDatasetId).listItems();

if (!items || items.length === 0) {
return NextResponse.json(
{ error: "Profile not found or account is private" },
{ status: 404 }
);
}

// Transform the first (and only) result to our format
const profileData = items[0] as any; // Type from Apify may vary

// Check if the account is private
if (profileData.private) {
return NextResponse.json(
{ error: "This account is private and cannot be scraped" },
{ status: 403 }
);
}

const transformedProfile = transformApifyToProfile(profileData);

return NextResponse.json(transformedProfile);
} catch (error) {
console.error("Error fetching Instagram profile:", error);

// Handle specific error cases
if (error instanceof Error) {
if (error.message.includes("APIFY_TOKEN")) {
return NextResponse.json(
{ error: "Apify configuration error. Please check your APIFY_TOKEN environment variable." },
{ status: 500 }
);
}
}

return NextResponse.json(
{ error: "Failed to fetch Instagram profile. Please try again later." },
{ status: 500 }
);
}
}
26 changes: 21 additions & 5 deletions social-demo/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,31 @@ import { EngagementChart } from "@/components/EngagementChart";
import { PostsGrid } from "@/components/PostsGrid";
import { HashtagCloud } from "@/components/HashtagCloud";
import { InstagramProfile } from "@/lib/types";
import { fetchInstagramProfile } from "@/data/mock-profile";


export default function Home() {
const [profile, setProfile] = useState<InstagramProfile | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSearch = async (username: string) => {
setIsLoading(true);
setError(null);
try {
// Simulate fetching data from Apify actor
// In production, this would call an API route that triggers the Apify actor
const data = await fetchInstagramProfile(username);
// Call the API route to fetch real Instagram data via Apify
const response = await fetch(`/api/instagram/${encodeURIComponent(username)}`);

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to fetch profile");
}

const data = await response.json();
setProfile(data);
} catch (error) {
console.error("Failed to fetch profile:", error);
// In production, show error message to user
setError(error instanceof Error ? error.message : "Failed to fetch profile");
setProfile(null);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -65,6 +74,13 @@ export default function Home() {
</p>
</div>
<SearchBar onSearch={handleSearch} isLoading={isLoading} />

{/* Error Message */}
{error && (
<div className="mt-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg text-center">
<p className="text-destructive font-medium">{error}</p>
</div>
)}
</div>

{/* Dashboard Content */}
Expand Down
Loading