diff --git a/docs/superpowers/specs/2026-04-23-instagram-facebook-variants-design.md b/docs/superpowers/specs/2026-04-23-instagram-facebook-variants-design.md new file mode 100644 index 00000000..673d60f5 --- /dev/null +++ b/docs/superpowers/specs/2026-04-23-instagram-facebook-variants-design.md @@ -0,0 +1,288 @@ +# Instagram and Facebook Content Variants in Post Editor + +**Date:** 2026-04-23 +**Branch:** feature/workspace-billing-plans + +## Problem + +The post editor has per-platform settings for TikTok (`TikTokSettings.vue`) but no equivalent UI for choosing content variants on Instagram (Feed / Reel / Story) and Facebook (Post / Reel / Story). The backend already supports these variants — `ContentType` enum maps to publisher branches in `InstagramPublisher` and `FacebookPublisher`, `UpdatePost` persists `content_type`, and `UpdatePostRequest` validates it — but `SyncPostPlatforms` always defaults to `InstagramFeed` / `FacebookPost` and the frontend has no way for users to change it. + +Users currently can only publish Feed posts on Instagram and standard Posts on Facebook, even when they want Reels or Stories. + +## Goals + +- Let users choose content variant per connected Instagram and Facebook account on the post edit page. +- Match the visual pattern established by `TikTokSettings.vue` (collapsible card with platform context). +- Support multiple connected accounts of the same platform (each card is independent — user can pick different variants for different accounts in the same post). +- Block submission with a clear warning when the chosen variant is incompatible with the attached media (mirrors existing `tiktokComplianceValid` pattern). +- Centralize media rules in a single source of truth, re-used for validation and for per-card warnings. + +## Non-Goals + +- Backend changes (publishers already handle all variants). +- Variant support for platforms beyond Instagram and Facebook (LinkedIn, Pinterest, etc. come later). +- Restricting the media upload picker based on variant — the variant-driven accept/max logic stays on the validation side, not on the file input (user can still drop any media; we just warn + block publish). + +## Design + +### New Files + +- `resources/js/components/posts/editor/InstagramSettings.vue` — rendered for `Platform::Instagram` and `Platform::InstagramFacebook` (same variant set). +- `resources/js/components/posts/editor/FacebookSettings.vue` — rendered for `Platform::Facebook`. +- `resources/js/composables/useMediaRules.ts` — restored from `main`, single source of truth for per-`content_type` media constraints. + +### Modified Files + +- `resources/js/pages/posts/Edit.vue` — add `platformContentTypes` state, compliance computeds, update `getSubmitData()` and autosave watcher, pipe new state into `ScheduleTab` and `PreviewTab`. +- `resources/js/components/posts/editor/ScheduleTab.vue` — accept `platformContentTypes` prop, emit `update:platform-content-type`, render `InstagramSettings` / `FacebookSettings` blocks per selected account. +- `resources/js/components/posts/editor/PreviewTab.vue` — read variant from `platformContentTypes[previewPlatform.id]` so preview updates live when user toggles variants. +- `lang/en/posts.php`, `lang/es/posts.php`, `lang/pt-BR/posts.php` — add translation keys for settings card, variant labels, warnings. + +### Component Structure (`InstagramSettings.vue`, `FacebookSettings.vue`) + +Mirror `TikTokSettings.vue`'s outer shell: + +- Collapsible root card with border. +- Header button: platform icon + `"Instagram settings"` / `"Facebook settings"` + chevron up/down. +- Closed by default; clicking the header toggles `open` ref. +- Body (when open): + - "Posting to" row: `Avatar` component + display name + `@username` of the connected account (critical when the user has multiple accounts of the same platform — makes clear which card configures which account). + - Variant picker: three horizontal pill buttons, one per variant. Active pill gets `bg-primary/10 border-primary`; inactive gets muted styling. Clicking emits `update:content-type` with the new value. + - Warning block (conditionally rendered): when `useMediaRules(contentType)` indicates the current `media` array violates the rules, render an `IconAlertTriangle` + short i18n message explaining the specific violation ("Reel requires a video", "Story accepts only 1 media", etc.). + +Props: + +```ts +interface Props { + socialAccount: SocialAccount | null; + contentType: string; // e.g. 'instagram_reel' + media: MediaItem[]; // to evaluate compliance warnings + disabled?: boolean; +} +``` + +Emits: + +```ts +'update:content-type': [value: string] +``` + +### Variant Catalog + +| Platform | Variants | Default | +|----------|----------|---------| +| Instagram (standalone + via Facebook) | `instagram_feed`, `instagram_reel`, `instagram_story` | `instagram_feed` | +| Facebook | `facebook_post`, `facebook_reel`, `facebook_story` | `facebook_post` | + +Labels and descriptions are already defined in `App\Enums\PostPlatform\ContentType` (`label()` and `description()` methods). Frontend uses i18n keys for display so strings stay in translation files, not in enum PHP strings. + +### State Management in `Edit.vue` + +Add a `platformContentTypes` ref that mirrors the existing `platformMeta` pattern exactly: + +```ts +const platformContentTypes = ref>( + Object.fromEntries( + post.value.post_platforms.map((pp) => [pp.id, pp.content_type ?? '']), + ), +); + +const updatePlatformContentType = (platformId: string, contentType: string) => { + platformContentTypes.value = { + ...platformContentTypes.value, + [platformId]: contentType, + }; +}; +``` + +This produces a dictionary keyed by `post_platform.id`, so two Instagram accounts get two independent entries. + +Update `getSubmitData()` to read `content_type` from state instead of the prop: + +```ts +const platforms = post.value.post_platforms + .filter((pp) => selectedPlatformIds.value.includes(pp.id)) + .map((pp) => ({ + id: pp.id, + content_type: platformContentTypes.value[pp.id] ?? pp.content_type, + meta: platformMeta.value[pp.id] ?? pp.meta ?? {}, + })); +``` + +Add `platformContentTypes` to the autosave `watch`: + +```ts +watch( + [content, media, selectedPlatformIds, scheduledDateTime, selectedLabelIds, platformMeta, platformContentTypes], + triggerAutosave, + { deep: true }, +); +``` + +### Media Rules Composable + +Restore `resources/js/composables/useMediaRules.ts` from `main` (the file already covered Instagram, Facebook, and every other platform correctly). The restored file keeps its original `useMediaRules(contentTypeRef)` reactive form (used inside settings components) and adds a small non-reactive helper for use in `Edit.vue`'s validation computeds: + +- `useMediaRules(contentTypeRef: Ref)` — reactive, already present on `main`, for use in `