From d60ef29835a60d48cba1770fd54fd51ce251782c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:01:38 +0000 Subject: [PATCH 1/3] Initial plan From 7b63e8feac369a058e2db003b2d35423607bc892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:10:03 +0000 Subject: [PATCH 2/3] Add Apify Instagram Profile Scraper integration Co-authored-by: lukas-bekr <224167845+lukas-bekr@users.noreply.github.com> --- social-demo/.env.example | 3 + social-demo/README.md | 169 ++++++++++++++++++ .../app/api/instagram/[username]/route.ts | 86 +++++++++ social-demo/app/page.tsx | 26 ++- social-demo/lib/apify-transform.ts | 116 ++++++++++++ social-demo/package.json | 1 + 6 files changed, 396 insertions(+), 5 deletions(-) create mode 100644 social-demo/.env.example create mode 100644 social-demo/README.md create mode 100644 social-demo/app/api/instagram/[username]/route.ts create mode 100644 social-demo/lib/apify-transform.ts diff --git a/social-demo/.env.example b/social-demo/.env.example new file mode 100644 index 0000000..07da8d5 --- /dev/null +++ b/social-demo/.env.example @@ -0,0 +1,3 @@ +# Apify API Token +# Get your token from https://console.apify.com/account/integrations +APIFY_TOKEN=your_apify_token_here diff --git a/social-demo/README.md b/social-demo/README.md new file mode 100644 index 0000000..d072c98 --- /dev/null +++ b/social-demo/README.md @@ -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. diff --git a/social-demo/app/api/instagram/[username]/route.ts b/social-demo/app/api/instagram/[username]/route.ts new file mode 100644 index 0000000..361e22e --- /dev/null +++ b/social-demo/app/api/instagram/[username]/route.ts @@ -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 } + ); + } +} diff --git a/social-demo/app/page.tsx b/social-demo/app/page.tsx index 0f586f2..a14dfbf 100644 --- a/social-demo/app/page.tsx +++ b/social-demo/app/page.tsx @@ -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(null); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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); } @@ -65,6 +74,13 @@ export default function Home() {

+ + {/* Error Message */} + {error && ( +
+

{error}

+
+ )} {/* Dashboard Content */} diff --git a/social-demo/lib/apify-transform.ts b/social-demo/lib/apify-transform.ts new file mode 100644 index 0000000..a392854 --- /dev/null +++ b/social-demo/lib/apify-transform.ts @@ -0,0 +1,116 @@ +import { InstagramProfile, InstagramPost, HashtagData, EngagementData } from "./types"; + +// Type definitions for Apify Instagram Profile Scraper output +interface ApifyPost { + id: string; + type: string; + shortCode: string; + caption: string; + displayUrl: string; + videoUrl?: string; + likesCount: number; + commentsCount: number; + timestamp: string; + hashtags?: string[]; +} + +interface ApifyProfileData { + username: string; + fullName: string; + biography: string; + profilePicUrl?: string; + profilePicUrlHD?: string; + followersCount: number; + followsCount: number; + postsCount: number; + externalUrl?: string; + verified: boolean; + private: boolean; + latestPosts?: ApifyPost[]; +} + +/** + * Transforms Apify Instagram Profile Scraper output to our InstagramProfile type + */ +export function transformApifyToProfile(apifyData: ApifyProfileData): InstagramProfile { + // Transform posts + const recentPosts: InstagramPost[] = (apifyData.latestPosts || []).slice(0, 10).map((post) => { + const totalEngagement = post.likesCount + post.commentsCount; + const engagementRate = apifyData.followersCount > 0 + ? (totalEngagement / apifyData.followersCount) * 100 + : 0; + + return { + id: post.id, + imageUrl: post.displayUrl || "", + videoUrl: post.videoUrl, + caption: post.caption || "", + likesCount: post.likesCount || 0, + commentsCount: post.commentsCount || 0, + timestamp: post.timestamp || new Date().toISOString(), + engagementRate: parseFloat(engagementRate.toFixed(2)), + type: post.type === "Video" || post.videoUrl ? "video" : post.type === "Sidecar" ? "image" : "image", + }; + }); + + // Extract hashtags from posts + const hashtagMap = new Map(); + apifyData.latestPosts?.forEach((post) => { + post.hashtags?.forEach((tag) => { + const normalizedTag = tag.startsWith("#") ? tag : `#${tag}`; + hashtagMap.set(normalizedTag, (hashtagMap.get(normalizedTag) || 0) + 1); + }); + }); + + const topHashtags: HashtagData[] = Array.from(hashtagMap.entries()) + .map(([tag, count]) => ({ tag, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 12); + + // Calculate metrics from recent posts + const avgLikes = recentPosts.length > 0 + ? Math.round(recentPosts.reduce((sum, post) => sum + post.likesCount, 0) / recentPosts.length) + : 0; + + const avgComments = recentPosts.length > 0 + ? Math.round(recentPosts.reduce((sum, post) => sum + post.commentsCount, 0) / recentPosts.length) + : 0; + + const avgEngagementRate = recentPosts.length > 0 + ? parseFloat((recentPosts.reduce((sum, post) => sum + post.engagementRate, 0) / recentPosts.length).toFixed(2)) + : 0; + + // Create engagement history from recent posts (simplified) + const engagementHistory: EngagementData[] = recentPosts.slice(0, 15).reverse().map((post) => { + const date = new Date(post.timestamp); + return { + date: date.toISOString().split('T')[0], + rate: post.engagementRate, + likes: post.likesCount, + comments: post.commentsCount, + }; + }); + + return { + username: apifyData.username, + fullName: apifyData.fullName || apifyData.username, + bio: apifyData.biography || "", + profilePicUrl: apifyData.profilePicUrlHD || apifyData.profilePicUrl || "", + followersCount: apifyData.followersCount || 0, + followingCount: apifyData.followsCount || 0, + postsCount: apifyData.postsCount || 0, + externalUrl: apifyData.externalUrl, + isVerified: apifyData.verified || false, + isPrivate: apifyData.private || false, + metrics: { + engagementRate: avgEngagementRate, + avgLikesPerPost: avgLikes, + avgCommentsPerPost: avgComments, + followerGrowth: 0, // Not available from single scrape + reachGrowth: 0, // Not available from single scrape + }, + recentPosts, + topHashtags, + engagementHistory, + }; +} diff --git a/social-demo/package.json b/social-demo/package.json index 3bf0fe2..6f89b68 100644 --- a/social-demo/package.json +++ b/social-demo/package.json @@ -12,6 +12,7 @@ "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "apify-client": "^2.19.0", "autoprefixer": "^10.4.22", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", From cbf3c7003f6200d95f298ae2a02ca11271d7b381 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:14:50 +0000 Subject: [PATCH 3/3] Configure Next.js image domains for Instagram CDN Co-authored-by: lukas-bekr <224167845+lukas-bekr@users.noreply.github.com> --- social-demo/next.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/social-demo/next.config.ts b/social-demo/next.config.ts index 6dc033e..e99e3f0 100644 --- a/social-demo/next.config.ts +++ b/social-demo/next.config.ts @@ -19,6 +19,15 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "picsum.photos", }, + // Instagram CDN patterns for profile pictures and posts + { + protocol: "https", + hostname: "**.fbcdn.net", + }, + { + protocol: "https", + hostname: "scontent-*.cdninstagram.com", + }, ], }, };