Skip to main content

Overview

The Postiz Calendar is your command center for managing all scheduled social media content. It provides a visual, interactive interface for viewing, creating, editing, and rescheduling posts across all your connected channels.

Calendar Views

Postiz offers multiple calendar views to help you manage your content effectively:
The default view shows a full week with hourly time slots. Perfect for detailed planning and visualizing your posting schedule.
// Week view displays 7 days with hourly rows
const currentWeek = dayjs.utc(startDate);
const days = Array.from({ length: 7 }, (_, i) => 
  currentWeek.add(i, 'day')
);

Calendar Interface Components

The left sidebar displays all your connected social media channels:
interface Integration {
  id: string;
  name: string;           // Channel name
  identifier: string;     // Platform type: 'x', 'linkedin', 'facebook'
  picture: string;        // Profile picture
  disabled: boolean;      // Channel status
  inBetweenSteps: boolean; // Setup incomplete
  refreshNeeded: boolean;  // Token expired
  time: Array<{ time: number }>; // Time slots
}
  • Active - Channel is connected and ready
  • Disabled - Channel is temporarily disabled
  • Setup Incomplete - Orange warning indicator
  • Refresh Needed - Red indicator, click to reconnect
{(integration.inBetweenSteps || integration.refreshNeeded) && (
  <div className="bg-red-500 w-[15px] h-[15px] rounded-full">
    !
  </div>
)}

Main Calendar Grid

The calendar grid displays posts organized by date and time:
1

Time Rows

Each row represents a time slot (hour in week view, time slot in day view).
// Hours are generated for 24-hour display
export const hours = Array.from({ length: 24 }, (_, i) => i);

// Convert to local format (12h or 24h)
const convertTimeFormatBasedOnLocality = (time: number) => {
  if (isUSCitizen()) {
    return `${time === 12 ? 12 : time % 12}:00 ${time >= 12 ? 'PM' : 'AM'}`;
  }
  return `${time}:00`;
};
2

Post Cards

Posts appear as cards in the calendar grid with preview content.
interface Post {
  id: string;
  group: string;           // Groups related posts
  content: string;
  publishDate: string;     // ISO date string
  state: State;           // DRAFT, SCHEDULED, PUBLISHED, etc.
  integration: {
    id: string;
    name: string;
    picture: string;
    providerIdentifier: string;
  };
  image?: Array<{
    id: string;
    path: string;
  }>;
}

Drag-and-Drop Scheduling

Reschedule posts by dragging them to new time slots:

Setting Up Drag Source

import { useDrag } from 'react-dnd';

const [{ isDragging }, drag] = useDrag(() => ({
  type: 'post',
  item: {
    post,
    oldDate: post.publishDate
  },
  collect: (monitor) => ({
    isDragging: monitor.isDragging()
  })
}));

Setting Up Drop Target

import { useDrop } from 'react-dnd';

const [{ isOver, canDrop }, drop] = useDrop(() => ({
  accept: 'post',
  drop: async (item: { post: Post }) => {
    // Calculate new date from drop position
    const newDate = calculateNewDate(day, hour);
    
    // Update via API
    await fetch(`/posts/${item.post.id}/date`, {
      method: 'PUT',
      body: JSON.stringify({ 
        date: newDate,
        action: 'update'
      })
    });
    
    // Refresh calendar
    reloadCalendarView();
  },
  collect: (monitor) => ({
    isOver: monitor.isOver(),
    canDrop: monitor.canDrop()
  })
}));
Posts cannot be dragged to past dates or times. The drop zone will indicate if a slot is invalid.

Post Interactions

Context Menu Actions

Right-click or click on any post to access actions:
const usePostActions = () => {
  const editPost = useCallback((post) => async () => {
    // Fetch full post data
    const data = await fetch(`/posts/group/${post.group}`).json();
    
    // Open edit modal
    modal.openModal({
      children: <AddEditModal 
        existingData={data}
        integrations={[integration]}
        date={dayjs.utc(post.publishDate).local()}
      />
    });
  }, []);
  
  const deletePost = useCallback((post) => async () => {
    if (!await deleteDialog('Delete this post?')) return;
    
    await fetch(`/posts/${post.group}`, {
      method: 'DELETE'
    });
    
    mutate();
  }, []);
  
  const duplicatePost = useCallback((post) => async () => {
    // Load post data
    const data = await fetch(`/posts/group/${post.group}`).json();
    
    // Find next available slot
    const { date } = await fetch('/posts/find-slot').json();
    
    // Open with pre-filled content but new date
    modal.openModal({
      children: <AddEditModal 
        onlyValues={data.posts}
        date={dayjs.utc(date).local()}
      />
    });
  }, []);
  
  const viewStatistics = useCallback((postId) => () => {
    modal.openModal({
      children: <StatisticsModal postId={postId} />
    });
  }, []);
  
  return { editPost, deletePost, duplicatePost, viewStatistics };
};

Keyboard Shortcuts

  • Delete - Delete selected post (after confirmation)
  • Escape - Close modal or deselect post
  • Arrow Keys - Navigate between posts (coming soon)
  • Ctrl/Cmd + D - Duplicate selected post (coming soon)

Calendar Navigation

Date Navigation

const CalendarNavigation = () => {
  const { startDate, setStartDate } = useCalendar();
  
  const previousWeek = () => {
    setStartDate(dayjs(startDate).subtract(7, 'days'));
  };
  
  const nextWeek = () => {
    setStartDate(dayjs(startDate).add(7, 'days'));
  };
  
  const today = () => {
    setStartDate(dayjs().startOf('week'));
  };
  
  return (
    <div className="flex gap-2">
      <button onClick={previousWeek}>Previous</button>
      <button onClick={today}>Today</button>
      <button onClick={nextWeek}>Next</button>
    </div>
  );
};

Quick Date Picker

Click the date header to open a date picker for quick navigation:
<DatePicker
  value={startDate}
  onChange={(date) => setStartDate(date)}
  minDate={dayjs().subtract(1, 'year')}
  maxDate={dayjs().add(1, 'year')}
/>

Filtering and Searching

Filter the calendar view by various criteria:

Channel Filter

const [selectedChannels, setSelectedChannels] = useState<string[]>([]);

// Filter posts by selected channels
const filteredPosts = posts.filter(post => 
  selectedChannels.length === 0 || 
  selectedChannels.includes(post.integration.id)
);

Tag Filter

// GET /posts/tags - Fetch available tags
const { tags } = await fetch('/posts/tags').json();

// Filter by tags
const filteredByTag = posts.filter(post => 
  post.tags?.some(tag => selectedTags.includes(tag.id))
);

State Filter

Show all posts regardless of state

Real-time Updates

The calendar automatically refreshes to show the latest post states:
// Polling interval for calendar updates
const { interval } = useInterval(() => {
  reloadCalendarView();
}, 30000); // Refresh every 30 seconds

// Manual refresh
const refreshCalendar = useCallback(() => {
  mutate(); // SWR revalidate
}, []);
The calendar polls for updates every 30 seconds when the tab is active. This ensures you always see the latest post states without manual refreshing.

Channel Management in Calendar

Adding Channels

const addChannel = () => {
  modal.openModal({
    title: 'Add Channel',
    children: <AddProviderButton onSuccess={mutate} />
  });
};

Organizing Channels

Drag channels to reorder them in the sidebar:
const [{ isDragging }, drag] = useDrag({
  type: 'menu',
  item: { id: integration.id }
});

const [{ isOver }, drop] = useDrop({
  accept: 'menu',
  drop: (item: { id: string }) => {
    changeItemGroup(item.id, group.id);
  }
});

Calendar Context API

The calendar uses React Context for state management:
interface CalendarContextType {
  startDate: string;
  setStartDate: (date: string) => void;
  posts: Post[];
  integrations: Integration[];
  reloadCalendarView: () => void;
  loading: boolean;
}

const CalendarContext = createContext<CalendarContextType>();

export const useCalendar = () => {
  const context = useContext(CalendarContext);
  if (!context) {
    throw new Error('useCalendar must be used within CalendarProvider');
  }
  return context;
};

Performance Optimization

  1. Virtualization - Only render visible time slots
  2. Memoization - Cache post calculations
  3. Debounced Drag - Reduce update frequency during dragging
  4. Lazy Loading - Load posts only for visible date range
const visiblePosts = useMemo(() => {
  return posts.filter(post => {
    const date = dayjs(post.publishDate);
    return date.isAfter(startDate) && 
           date.isBefore(endDate);
  });
}, [posts, startDate, endDate]);

Best Practices

Use Week View for Planning

Week view gives you the best overview for content planning and scheduling.

Organize Channels by Priority

Drag channels to reorder them with most important channels at the top.

Set Up Time Slots

Configure time slots for each channel to enable smart auto-scheduling.

Use Filters Effectively

Filter by channel or state to focus on specific content.

Next Steps

Post Scheduling

Learn how to create and schedule posts

Analytics

Track performance of your scheduled posts

Build docs developers (and LLMs) love