PostPost

Next.js

Build social media features into your Next.js app with PostPost API.

Installation

npm install next

No additional dependencies needed - uses native fetch.

Environment Setup

# .env.local
PUBLORA_API_KEY=sk_your_api_key_here

API Route Handler (App Router)

Create Post Endpoint

// app/api/social/post/route.ts
import { NextRequest, NextResponse } from 'next/server';

const PUBLORA_API_KEY = process.env.PUBLORA_API_KEY!;
const BASE_URL = 'https://api.postpost.dev/api/v1';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { content, platforms, scheduledTime } = body;

    if (!content || !platforms?.length) {
      return NextResponse.json(
        { error: 'Content and platforms are required' },
        { status: 400 }
      );
    }

    const response = await fetch(`${BASE_URL}/create-post`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': PUBLORA_API_KEY,
      },
      body: JSON.stringify({
        content,
        platforms,
        scheduledTime,
      }),
    });

    const data = await response.json();

    if (!response.ok) {
      return NextResponse.json(
        { error: data.error || data.message },
        { status: response.status }
      );
    }

    return NextResponse.json(data);
  } catch (error) {
    console.error('PostPost API error:', error);
    return NextResponse.json(
      { error: 'Failed to create post' },
      { status: 500 }
    );
  }
}

Get Connections Endpoint

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

const PUBLORA_API_KEY = process.env.PUBLORA_API_KEY!;
const BASE_URL = 'https://api.postpost.dev/api/v1';

export async function GET() {
  try {
    const response = await fetch(`${BASE_URL}/platform-connections`, {
      headers: {
        'x-api-key': PUBLORA_API_KEY,
      },
      next: { revalidate: 60 }, // Cache for 60 seconds
    });

    const data = await response.json();

    if (!response.ok) {
      return NextResponse.json(
        { error: data.error || data.message },
        { status: response.status }
      );
    }

    return NextResponse.json(data);
  } catch (error) {
    console.error('PostPost API error:', error);
    return NextResponse.json(
      { error: 'Failed to fetch connections' },
      { status: 500 }
    );
  }
}

Get Post Status Endpoint

// app/api/social/post/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

const PUBLORA_API_KEY = process.env.PUBLORA_API_KEY!;
const BASE_URL = 'https://api.postpost.dev/api/v1';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const response = await fetch(`${BASE_URL}/get-post/${params.id}`, {
      headers: {
        'x-api-key': PUBLORA_API_KEY,
      },
    });

    const data = await response.json();

    if (!response.ok) {
      return NextResponse.json(
        { error: data.error || data.message },
        { status: response.status }
      );
    }

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch post' },
      { status: 500 }
    );
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const response = await fetch(`${BASE_URL}/delete-post/${params.id}`, {
      method: 'DELETE',
      headers: {
        'x-api-key': PUBLORA_API_KEY,
      },
    });

    const data = await response.json();

    if (!response.ok) {
      return NextResponse.json(
        { error: data.error || data.message },
        { status: response.status }
      );
    }

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to delete post' },
      { status: 500 }
    );
  }
}

PostPost Service Class

// lib/postpost.ts
const PUBLORA_API_KEY = process.env.PUBLORA_API_KEY!;
const BASE_URL = 'https://api.postpost.dev/api/v1';

export interface PlatformConnection {
  platformId: string;
  platform: string;
  username: string;
  displayName: string;
}

export interface CreatePostRequest {
  content: string;
  platforms: string[];
  scheduledTime?: string;
}

export interface PostGroup {
  postGroupId: string;
  content: string;
  status: string;
  posts: Array<{
    platform: string;
    status: string;
    publishedUrl?: string;
    error?: string;
  }>;
}

class PostPostService {
  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': PUBLORA_API_KEY,
        ...options.headers,
      },
    });

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || data.message || 'API request failed');
    }

    return data;
  }

  async getConnections(): Promise<PlatformConnection[]> {
    const data = await this.request<{ connections: PlatformConnection[] }>(
      '/platform-connections'
    );
    return data.connections;
  }

  async createPost(request: CreatePostRequest): Promise<{ postGroupId: string }> {
    return this.request('/create-post', {
      method: 'POST',
      body: JSON.stringify(request),
    });
  }

  async getPost(postGroupId: string): Promise<PostGroup> {
    return this.request(`/get-post/${postGroupId}`);
  }

  async deletePost(postGroupId: string): Promise<{ success: boolean }> {
    return this.request(`/delete-post/${postGroupId}`, {
      method: 'DELETE',
    });
  }

  async getUploadUrl(fileName: string, contentType: string, postGroupId: string) {
    return this.request<{ uploadUrl: string; fileUrl: string; mediaId: string }>(
      '/get-upload-url',
      {
        method: 'POST',
        body: JSON.stringify({ fileName, contentType, postGroupId }),
      }
    );
  }
}

export const postpost = new PostPostService();

React Components

Social Post Form

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

import { useState, useEffect } from 'react';

interface Connection {
  platformId: string;
  platform: string;
  username: string;
}

export default function SocialPostForm() {
  const [content, setContent] = useState('');
  const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
  const [connections, setConnections] = useState<Connection[]>([]);
  const [scheduledTime, setScheduledTime] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [result, setResult] = useState<{ success?: boolean; postGroupId?: string; error?: string } | null>(null);

  useEffect(() => {
    fetch('/api/social/connections')
      .then((res) => res.json())
      .then((data) => setConnections(data.connections || []))
      .catch(console.error);
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setResult(null);

    try {
      const response = await fetch('/api/social/post', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          content,
          platforms: selectedPlatforms,
          scheduledTime: scheduledTime || undefined,
        }),
      });

      const data = await response.json();

      if (response.ok) {
        setResult({ success: true, postGroupId: data.postGroupId });
        setContent('');
        setSelectedPlatforms([]);
        setScheduledTime('');
      } else {
        setResult({ success: false, error: data.error });
      }
    } catch (error) {
      setResult({ success: false, error: 'Failed to create post' });
    } finally {
      setIsLoading(false);
    }
  };

  const togglePlatform = (platformId: string) => {
    setSelectedPlatforms((prev) =>
      prev.includes(platformId)
        ? prev.filter((p) => p !== platformId)
        : [...prev, platformId]
    );
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label className="block text-sm font-medium mb-1">Content</label>
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
          className="w-full p-2 border rounded"
          rows={4}
          required
        />
        <p className="text-sm text-gray-500 mt-1">{content.length} characters</p>
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Platforms</label>
        <div className="flex flex-wrap gap-2">
          {connections.map((conn) => (
            <button
              key={conn.platformId}
              type="button"
              onClick={() => togglePlatform(conn.platformId)}
              className={`px-3 py-1 rounded ${
                selectedPlatforms.includes(conn.platformId)
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200'
              }`}
            >
              {conn.platform}: {conn.username}
            </button>
          ))}
        </div>
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Schedule (optional)</label>
        <input
          type="datetime-local"
          value={scheduledTime}
          onChange={(e) => setScheduledTime(e.target.value)}
          className="p-2 border rounded"
        />
      </div>

      <button
        type="submit"
        disabled={isLoading || !content || selectedPlatforms.length === 0}
        className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
      >
        {isLoading ? 'Posting...' : 'Schedule Post'}
      </button>

      {result && (
        <div className={`p-3 rounded ${result.success ? 'bg-green-100' : 'bg-red-100'}`}>
          {result.success
            ? `Post created: ${result.postGroupId}`
            : `Error: ${result.error}`}
        </div>
      )}
    </form>
  );
}

Post Status Component

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

import { useState, useEffect } from 'react';

interface PostStatusProps {
  postGroupId: string;
}

interface Post {
  platform: string;
  status: string;
  publishedUrl?: string;
  error?: string;
}

interface PostGroup {
  postGroupId: string;
  status: string;
  posts: Post[];
}

export default function PostStatus({ postGroupId }: PostStatusProps) {
  const [postGroup, setPostGroup] = useState<PostGroup | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchStatus = async () => {
      try {
        const response = await fetch(`/api/social/post/${postGroupId}`);
        const data = await response.json();
        setPostGroup(data);
      } catch (error) {
        console.error('Failed to fetch post status:', error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchStatus();

    // Poll every 10 seconds if still processing
    const interval = setInterval(() => {
      if (postGroup?.status === 'scheduled' || postGroup?.status === 'processing') {
        fetchStatus();
      }
    }, 10000);

    return () => clearInterval(interval);
  }, [postGroupId, postGroup?.status]);

  if (isLoading) return <div>Loading...</div>;
  if (!postGroup) return <div>Post not found</div>;

  const statusColors: Record<string, string> = {
    published: 'bg-green-100 text-green-800',
    scheduled: 'bg-blue-100 text-blue-800',
    processing: 'bg-yellow-100 text-yellow-800',
    failed: 'bg-red-100 text-red-800',
    partially_published: 'bg-orange-100 text-orange-800',
  };

  return (
    <div className="border rounded p-4">
      <div className="flex items-center justify-between mb-4">
        <h3 className="font-medium">Post: {postGroupId}</h3>
        <span className={`px-2 py-1 rounded text-sm ${statusColors[postGroup.status] || 'bg-gray-100'}`}>
          {postGroup.status}
        </span>
      </div>

      <div className="space-y-2">
        {postGroup.posts.map((post, index) => (
          <div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded">
            <span className="font-medium">{post.platform}</span>
            <div className="text-right">
              <span className={`text-sm ${post.status === 'published' ? 'text-green-600' : post.status === 'failed' ? 'text-red-600' : 'text-gray-600'}`}>
                {post.status}
              </span>
              {post.publishedUrl && (
                <a href={post.publishedUrl} target="_blank" rel="noopener noreferrer" className="ml-2 text-blue-500 text-sm">
                  View
                </a>
              )}
              {post.error && <p className="text-red-500 text-xs">{post.error}</p>}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Server Actions (Next.js 14+)

// app/actions/social.ts
'use server';

import { postpost } from '@/lib/postpost';
import { revalidatePath } from 'next/cache';

export async function createSocialPost(formData: FormData) {
  const content = formData.get('content') as string;
  const platforms = formData.getAll('platforms') as string[];
  const scheduledTime = formData.get('scheduledTime') as string | null;

  if (!content || platforms.length === 0) {
    return { error: 'Content and at least one platform are required' };
  }

  try {
    const result = await postpost.createPost({
      content,
      platforms,
      scheduledTime: scheduledTime || undefined,
    });

    revalidatePath('/dashboard');
    return { success: true, postGroupId: result.postGroupId };
  } catch (error) {
    return { error: error instanceof Error ? error.message : 'Failed to create post' };
  }
}

export async function deleteSocialPost(postGroupId: string) {
  try {
    await postpost.deletePost(postGroupId);
    revalidatePath('/dashboard');
    return { success: true };
  } catch (error) {
    return { error: error instanceof Error ? error.message : 'Failed to delete post' };
  }
}

export async function getConnections() {
  try {
    return await postpost.getConnections();
  } catch (error) {
    console.error('Failed to get connections:', error);
    return [];
  }
}

Server Component with Data Fetching

// app/dashboard/page.tsx
import { postpost } from '@/lib/postpost';
import SocialPostForm from '@/components/SocialPostForm';

export default async function DashboardPage() {
  const connections = await postpost.getConnections();

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">Social Media Dashboard</h1>

      <div className="grid gap-6 md:grid-cols-2">
        <div>
          <h2 className="text-lg font-semibold mb-4">Connected Accounts</h2>
          <div className="space-y-2">
            {connections.map((conn) => (
              <div key={conn.platformId} className="p-3 border rounded flex justify-between">
                <span className="font-medium capitalize">{conn.platform}</span>
                <span className="text-gray-600">{conn.username}</span>
              </div>
            ))}
          </div>
        </div>

        <div>
          <h2 className="text-lg font-semibold mb-4">Create Post</h2>
          <SocialPostForm />
        </div>
      </div>
    </div>
  );
}

Middleware for API Protection

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Protect social API routes
  if (request.nextUrl.pathname.startsWith('/api/social')) {
    // Add your authentication logic here
    // For example, check for a session cookie or JWT

    const authHeader = request.headers.get('authorization');

    if (!authHeader) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/social/:path*',
};

Error Handling Hook

// hooks/usePostPost.ts
'use client';

import { useState, useCallback } from 'react';

interface UsePostPostOptions {
  onSuccess?: (data: any) => void;
  onError?: (error: string) => void;
}

export function usePostPost(options: UsePostPostOptions = {}) {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const createPost = useCallback(
    async (content: string, platforms: string[], scheduledTime?: string) => {
      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch('/api/social/post', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ content, platforms, scheduledTime }),
        });

        const data = await response.json();

        if (!response.ok) {
          throw new Error(data.error || 'Failed to create post');
        }

        options.onSuccess?.(data);
        return data;
      } catch (err) {
        const message = err instanceof Error ? err.message : 'Unknown error';
        setError(message);
        options.onError?.(message);
        throw err;
      } finally {
        setIsLoading(false);
      }
    },
    [options]
  );

  const getConnections = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/social/connections');
      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to fetch connections');
      }

      return data.connections;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Unknown error';
      setError(message);
      throw err;
    } finally {
      setIsLoading(false);
    }
  }, []);

  return {
    createPost,
    getConnections,
    isLoading,
    error,
  };
}

PostPost — Social media API with free tier, paid plans from $2.99/account

On this page