Back to Blog
Tutorial

Build a Social Media Analytics Dashboard in 2025

January 5, 2026
9 min read
S
By SociaVault Team
AnalyticsDashboardReactNext.jsData Visualization

Build a Social Media Analytics Dashboard

You want to track social media performance. Followers, engagement, growth trends—all in one place.

Let's build it.

By the end of this guide, you'll have a working dashboard that:

  • Displays real-time metrics from multiple platforms
  • Shows historical trends with charts
  • Updates automatically
  • Looks professional

Tech Stack

  • Next.js 14 - React framework
  • Tailwind CSS - Styling
  • Recharts - Charts
  • SociaVault API - Data source
  • Prisma + SQLite - Data storage

Project Setup

npx create-next-app@latest social-dashboard --typescript --tailwind --app
cd social-dashboard
npm install recharts @prisma/client date-fns
npm install -D prisma

Database Schema

npx prisma init --datasource-provider sqlite

Edit prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Account {
  id        Int      @id @default(autoincrement())
  platform  String
  username  String
  name      String?
  avatarUrl String?
  createdAt DateTime @default(now())
  
  snapshots Snapshot[]
  
  @@unique([platform, username])
}

model Snapshot {
  id           Int      @id @default(autoincrement())
  accountId    Int
  followers    Int
  following    Int?
  posts        Int?
  engagement   Float?
  collectedAt  DateTime @default(now())
  
  account Account @relation(fields: [accountId], references: [id])
  
  @@index([accountId, collectedAt])
}
npx prisma db push

API Routes

Fetch and Store Data

// app/api/collect/route.ts
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const API_KEY = process.env.SOCIAVAULT_API_KEY!;
const API_BASE = 'https://api.sociavault.com/v1/scrape';

async function fetchProfile(platform: string, username: string) {
  const response = await fetch(
    `${API_BASE}/${platform}/profile?username=${username}`,
    { headers: { 'Authorization': `Bearer ${API_KEY}` } }
  );
  
  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }
  
  return response.json();
}

export async function POST() {
  // Get all tracked accounts
  const accounts = await prisma.account.findMany();
  
  const results = [];
  
  for (const account of accounts) {
    try {
      const result = await fetchProfile(account.platform, account.username);
      const data = result.data;
      
      // Create snapshot
      const snapshot = await prisma.snapshot.create({
        data: {
          accountId: account.id,
          followers: data.follower_count || data.followers || 0,
          following: data.following_count || data.following || 0,
          posts: data.video_count || data.posts_count || 0,
          engagement: data.engagement_rate || null
        }
      });
      
      results.push({ account: account.username, success: true, snapshot });
      
      // Rate limiting
      await new Promise(r => setTimeout(r, 500));
      
    } catch (error: any) {
      results.push({ account: account.username, success: false, error: error.message });
    }
  }
  
  return NextResponse.json({ results });
}

Get Dashboard Data

// app/api/dashboard/route.ts
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { subDays } from 'date-fns';

const prisma = new PrismaClient();

export async function GET() {
  const thirtyDaysAgo = subDays(new Date(), 30);
  
  // Get accounts with latest snapshot
  const accounts = await prisma.account.findMany({
    include: {
      snapshots: {
        orderBy: { collectedAt: 'desc' },
        take: 1
      }
    }
  });
  
  // Get historical data for charts
  const history = await prisma.snapshot.findMany({
    where: {
      collectedAt: { gte: thirtyDaysAgo }
    },
    include: {
      account: true
    },
    orderBy: { collectedAt: 'asc' }
  });
  
  // Calculate totals
  const totalFollowers = accounts.reduce((sum, acc) => {
    return sum + (acc.snapshots[0]?.followers || 0);
  }, 0);
  
  // Calculate growth
  const oldestSnapshots = await prisma.snapshot.findMany({
    where: {
      collectedAt: { gte: thirtyDaysAgo, lte: subDays(new Date(), 29) }
    },
    distinct: ['accountId']
  });
  
  const oldTotal = oldestSnapshots.reduce((sum, s) => sum + s.followers, 0);
  const growth = oldTotal > 0 ? ((totalFollowers - oldTotal) / oldTotal) * 100 : 0;
  
  return NextResponse.json({
    accounts: accounts.map(a => ({
      ...a,
      currentFollowers: a.snapshots[0]?.followers || 0
    })),
    history,
    totals: {
      followers: totalFollowers,
      growth: growth.toFixed(2),
      accounts: accounts.length
    }
  });
}

Add Account

// app/api/accounts/route.ts
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const API_KEY = process.env.SOCIAVAULT_API_KEY!;

export async function POST(request: Request) {
  const { platform, username } = await request.json();
  
  // Fetch profile to validate and get data
  const response = await fetch(
    `https://api.sociavault.com/v1/scrape/${platform}/profile?username=${username}`,
    { headers: { 'Authorization': `Bearer ${API_KEY}` } }
  );
  
  if (!response.ok) {
    return NextResponse.json({ error: 'Account not found' }, { status: 404 });
  }
  
  const result = await response.json();
  const data = result.data;
  
  // Create or update account
  const account = await prisma.account.upsert({
    where: {
      platform_username: { platform, username }
    },
    create: {
      platform,
      username,
      name: data.nickname || data.full_name || data.name,
      avatarUrl: data.avatar_url || data.profile_pic_url
    },
    update: {
      name: data.nickname || data.full_name || data.name,
      avatarUrl: data.avatar_url || data.profile_pic_url
    }
  });
  
  // Create initial snapshot
  await prisma.snapshot.create({
    data: {
      accountId: account.id,
      followers: data.follower_count || data.followers || 0,
      following: data.following_count || data.following || 0,
      posts: data.video_count || data.posts_count || 0
    }
  });
  
  return NextResponse.json({ account });
}

Dashboard Components

Main Dashboard Page

// app/dashboard/page.tsx
'use client';

import { useEffect, useState } from 'react';
import { StatsCards } from '@/components/StatsCards';
import { FollowersChart } from '@/components/FollowersChart';
import { AccountsList } from '@/components/AccountsList';
import { AddAccount } from '@/components/AddAccount';

interface DashboardData {
  accounts: any[];
  history: any[];
  totals: {
    followers: number;
    growth: string;
    accounts: number;
  };
}

export default function Dashboard() {
  const [data, setData] = useState<DashboardData | null>(null);
  const [loading, setLoading] = useState(true);
  
  const fetchData = async () => {
    const response = await fetch('/api/dashboard');
    const json = await response.json();
    setData(json);
    setLoading(false);
  };
  
  useEffect(() => {
    fetchData();
  }, []);
  
  if (loading) {
    return <div className="p-8">Loading...</div>;
  }
  
  return (
    <div className="min-h-screen bg-gray-100 p-8">
      <div className="max-w-7xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">Social Media Dashboard</h1>
        
        <StatsCards totals={data!.totals} />
        
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
          <div className="lg:col-span-2">
            <FollowersChart history={data!.history} />
          </div>
          <div>
            <AddAccount onAdd={fetchData} />
          </div>
        </div>
        
        <AccountsList accounts={data!.accounts} />
      </div>
    </div>
  );
}

Stats Cards

// components/StatsCards.tsx
interface StatsCardsProps {
  totals: {
    followers: number;
    growth: string;
    accounts: number;
  };
}

export function StatsCards({ totals }: StatsCardsProps) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      <div className="bg-white rounded-lg p-6 shadow">
        <p className="text-sm text-gray-500">Total Followers</p>
        <p className="text-3xl font-bold">
          {totals.followers.toLocaleString()}
        </p>
      </div>
      
      <div className="bg-white rounded-lg p-6 shadow">
        <p className="text-sm text-gray-500">30-Day Growth</p>
        <p className={`text-3xl font-bold ${
          parseFloat(totals.growth) >= 0 ? 'text-green-600' : 'text-red-600'
        }`}>
          {parseFloat(totals.growth) >= 0 ? '+' : ''}{totals.growth}%
        </p>
      </div>
      
      <div className="bg-white rounded-lg p-6 shadow">
        <p className="text-sm text-gray-500">Tracked Accounts</p>
        <p className="text-3xl font-bold">{totals.accounts}</p>
      </div>
    </div>
  );
}

Followers Chart

// components/FollowersChart.tsx
'use client';

import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer
} from 'recharts';
import { format } from 'date-fns';

interface FollowersChartProps {
  history: any[];
}

export function FollowersChart({ history }: FollowersChartProps) {
  // Group by date and sum followers
  const chartData = history.reduce((acc: any[], snapshot) => {
    const date = format(new Date(snapshot.collectedAt), 'MMM dd');
    const existing = acc.find(d => d.date === date);
    
    if (existing) {
      existing[snapshot.account.platform] = 
        (existing[snapshot.account.platform] || 0) + snapshot.followers;
    } else {
      acc.push({
        date,
        [snapshot.account.platform]: snapshot.followers
      });
    }
    
    return acc;
  }, []);
  
  const platforms = [...new Set(history.map(h => h.account.platform))];
  const colors: Record<string, string> = {
    tiktok: '#00f2ea',
    instagram: '#E1306C',
    youtube: '#FF0000',
    twitter: '#1DA1F2'
  };
  
  return (
    <div className="bg-white rounded-lg p-6 shadow">
      <h2 className="text-lg font-semibold mb-4">Follower Trends</h2>
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip />
          <Legend />
          {platforms.map(platform => (
            <Line
              key={platform}
              type="monotone"
              dataKey={platform}
              stroke={colors[platform] || '#8884d8'}
              name={platform.charAt(0).toUpperCase() + platform.slice(1)}
            />
          ))}
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

Accounts List

// components/AccountsList.tsx
interface AccountsListProps {
  accounts: any[];
}

export function AccountsList({ accounts }: AccountsListProps) {
  const platformColors: Record<string, string> = {
    tiktok: 'bg-black',
    instagram: 'bg-gradient-to-r from-purple-500 to-pink-500',
    youtube: 'bg-red-600',
    twitter: 'bg-blue-400'
  };
  
  return (
    <div className="bg-white rounded-lg shadow mt-8">
      <div className="p-6 border-b">
        <h2 className="text-lg font-semibold">Tracked Accounts</h2>
      </div>
      <div className="divide-y">
        {accounts.map(account => (
          <div key={account.id} className="p-4 flex items-center justify-between">
            <div className="flex items-center gap-4">
              <div className={`w-10 h-10 rounded-full ${platformColors[account.platform]} flex items-center justify-center`}>
                {account.avatarUrl ? (
                  <img 
                    src={account.avatarUrl} 
                    alt={account.username}
                    className="w-10 h-10 rounded-full"
                  />
                ) : (
                  <span className="text-white text-sm font-bold">
                    {account.platform[0].toUpperCase()}
                  </span>
                )}
              </div>
              <div>
                <p className="font-medium">{account.name || account.username}</p>
                <p className="text-sm text-gray-500">
                  @{account.username} · {account.platform}
                </p>
              </div>
            </div>
            <div className="text-right">
              <p className="font-semibold">
                {account.currentFollowers.toLocaleString()}
              </p>
              <p className="text-sm text-gray-500">followers</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Add Account Form

// components/AddAccount.tsx
'use client';

import { useState } from 'react';

interface AddAccountProps {
  onAdd: () => void;
}

export function AddAccount({ onAdd }: AddAccountProps) {
  const [platform, setPlatform] = useState('tiktok');
  const [username, setUsername] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    
    try {
      const response = await fetch('/api/accounts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ platform, username })
      });
      
      if (!response.ok) {
        throw new Error('Account not found');
      }
      
      setUsername('');
      onAdd();
    } catch (err: any) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="bg-white rounded-lg p-6 shadow">
      <h2 className="text-lg font-semibold mb-4">Add Account</h2>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">
            Platform
          </label>
          <select
            value={platform}
            onChange={(e) => setPlatform(e.target.value)}
            className="w-full border rounded-lg p-2"
          >
            <option value="tiktok">TikTok</option>
            <option value="instagram">Instagram</option>
            <option value="youtube">YouTube</option>
            <option value="twitter">Twitter</option>
          </select>
        </div>
        
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">
            Username
          </label>
          <input
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            placeholder="charlidamelio"
            className="w-full border rounded-lg p-2"
          />
        </div>
        
        {error && (
          <p className="text-red-500 text-sm">{error}</p>
        )}
        
        <button
          type="submit"
          disabled={loading || !username}
          className="w-full bg-blue-600 text-white rounded-lg p-2 hover:bg-blue-700 disabled:opacity-50"
        >
          {loading ? 'Adding...' : 'Add Account'}
        </button>
      </form>
    </div>
  );
}

Automatic Updates

Set up a cron job or use Vercel Cron:

// app/api/cron/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // Verify cron secret
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  // Trigger collection
  const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/collect`, {
    method: 'POST'
  });
  
  return NextResponse.json({ success: true });
}

Add to vercel.json:

{
  "crons": [{
    "path": "/api/cron",
    "schedule": "0 */6 * * *"
  }]
}

Deploy

# Set environment variables
echo "SOCIAVAULT_API_KEY=your_key_here" >> .env.local

# Deploy to Vercel
npx vercel

Start building your dashboard today!

Get your API key at sociavault.com with 50 free credits.


Related:

Found this helpful?

Share it with others who might benefit

Ready to Try SociaVault?

Start extracting social media data with our powerful API. No credit card required.