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.