trypost/resources/js/pages/posts/Edit.vue

940 lines
36 KiB
Vue

<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import { Clock, AlertCircle, Trash2, Send, Link2, MoreHorizontal, Plus } from 'lucide-vue-next';
import axios from 'axios';
import dayjs from '@/dayjs';
import { PlatformPreview } from '@/components/posts/previews';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import DatePicker from '@/components/DatePicker.vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
import { calendar } from '@/routes';
import { destroy as destroyPost, update as updatePost } from '@/routes/posts';
import { store as storeMedia, destroy as destroyMedia, duplicate as duplicateMedia } from '@/routes/medias';
import { type BreadcrumbItemType } from '@/types';
interface SocialAccount {
id: string;
platform: string;
display_name: string;
username: string;
avatar_url: string | null;
}
interface MediaItem {
id: string;
url: string;
type: string;
original_filename: string;
}
interface PostPlatform {
id: string;
social_account_id: string;
enabled: boolean;
platform: string;
content: string;
content_type: string | null;
status: string;
social_account: SocialAccount;
media: MediaItem[];
}
interface ContentTypeOption {
value: string;
label: string;
description: string;
}
interface Post {
id: string;
status: string;
synced: boolean;
scheduled_at: string | null;
post_platforms: PostPlatform[];
}
interface PlatformConfig {
maxContentLength: number;
maxImages: number;
allowedMediaTypes: string[];
supportsTextOnly: boolean;
}
interface Workspace {
id: string;
name: string;
timezone: string;
}
interface Props {
workspace: Workspace;
post: Post;
socialAccounts: SocialAccount[];
platformConfigs: Record<string, PlatformConfig>;
}
const props = defineProps<Props>();
const breadcrumbs: BreadcrumbItemType[] = [
{ title: 'Calendar', href: calendar.url() },
{ title: 'Edit Post', href: '#' },
];
// State
const selectedPlatformIds = ref<string[]>(
props.post.post_platforms.filter(pp => pp.enabled).map(pp => pp.id)
);
const synced = ref(props.post.synced);
const enabledPlatforms = props.post.post_platforms.filter(pp => pp.enabled);
const globalContent = ref(
enabledPlatforms[0]?.content || props.post.post_platforms[0]?.content || ''
);
const platformContents = ref<Record<string, string>>(
Object.fromEntries(props.post.post_platforms.map(pp => [pp.id, pp.content]))
);
const platformMedia = ref<Record<string, MediaItem[]>>(
Object.fromEntries(props.post.post_platforms.map(pp => [pp.id, pp.media || []]))
);
const platformContentTypes = ref<Record<string, string>>(
Object.fromEntries(props.post.post_platforms.map(pp => [pp.id, pp.content_type || getDefaultContentType(pp.platform)]))
);
const isUploading = ref<Record<string, boolean>>({});
const isSubmitting = ref(false);
const deleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
const showEnableSyncDialog = ref(false);
const showDisableSyncDialog = ref(false);
const showPlatformsDialog = ref(false);
// Active tab - first selected platform
const activeTabId = ref<string>(selectedPlatformIds.value[0] || props.post.post_platforms[0]?.id || '');
// Watch selected platforms and update active tab if needed
watch(selectedPlatformIds, (newIds) => {
if (!newIds.includes(activeTabId.value) && newIds.length > 0) {
activeTabId.value = newIds[0];
}
}, { deep: true });
// Convert UTC to workspace timezone for display
const getLocalSchedule = () => {
if (!props.post.scheduled_at) {
return { date: '', time: '09:00' };
}
const local = dayjs.utc(props.post.scheduled_at).tz(props.workspace.timezone);
return {
date: local.format('YYYY-MM-DD'),
time: local.format('HH:mm'),
};
};
const { date: initialDate, time: initialTime } = getLocalSchedule();
const scheduledDate = ref(initialDate);
const scheduledTime = ref(initialTime);
const timezoneAbbr = computed(() => {
return dayjs().tz(props.workspace.timezone).format('z');
});
// Helpers
const getPlatformLogo = (platform: string): string => {
const logos: Record<string, string> = {
'linkedin': '/images/accounts/linkedin.png',
'linkedin-page': '/images/accounts/linkedin.png',
'x': '/images/accounts/x.png',
'tiktok': '/images/accounts/tiktok.png',
'youtube': '/images/accounts/youtube.png',
'facebook': '/images/accounts/facebook.png',
'instagram': '/images/accounts/instagram.png',
'threads': '/images/accounts/threads.png',
};
return logos[platform] || '/images/accounts/default.png';
};
const getPlatformLabel = (platform: string): string => {
const labels: Record<string, string> = {
'linkedin': 'LinkedIn',
'linkedin-page': 'LinkedIn Page',
'x': 'X',
'tiktok': 'TikTok',
'youtube': 'YouTube Shorts',
'facebook': 'Facebook Page',
'instagram': 'Instagram',
'threads': 'Threads',
};
return labels[platform] || platform;
};
// Content type options per platform
const contentTypeOptions: Record<string, ContentTypeOption[]> = {
'instagram': [
{ value: 'instagram_feed', label: 'Feed Post', description: 'Appears in your feed and profile' },
{ value: 'instagram_reel', label: 'Reel', description: 'Short video up to 90 seconds' },
{ value: 'instagram_story', label: 'Story', description: 'Disappears after 24 hours' },
],
'linkedin': [
{ value: 'linkedin_post', label: 'Post', description: 'Standard post with text and media' },
{ value: 'linkedin_carousel', label: 'Carousel', description: 'Swipeable images' },
],
'linkedin-page': [
{ value: 'linkedin_page_post', label: 'Post', description: 'Standard post with text and media' },
{ value: 'linkedin_page_carousel', label: 'Carousel', description: 'Swipeable images' },
],
'facebook': [
{ value: 'facebook_post', label: 'Post', description: 'Standard post on your page' },
{ value: 'facebook_reel', label: 'Reel', description: 'Short video up to 90 seconds' },
{ value: 'facebook_story', label: 'Story', description: 'Disappears after 24 hours' },
],
'tiktok': [
{ value: 'tiktok_video', label: 'Video', description: 'Short-form video content' },
],
'youtube': [
{ value: 'youtube_short', label: 'Short', description: 'Vertical video up to 60 seconds' },
],
'x': [
{ value: 'x_post', label: 'Post', description: 'Tweet with text and media' },
],
'threads': [
{ value: 'threads_post', label: 'Post', description: 'Text post with optional media' },
],
};
function getDefaultContentType(platform: string): string {
const defaults: Record<string, string> = {
'instagram': 'instagram_feed',
'linkedin': 'linkedin_post',
'linkedin-page': 'linkedin_page_post',
'facebook': 'facebook_post',
'tiktok': 'tiktok_video',
'youtube': 'youtube_short',
'x': 'x_post',
'threads': 'threads_post',
};
return defaults[platform] || '';
}
function getContentTypeOptions(platform: string): ContentTypeOption[] {
return contentTypeOptions[platform] || [];
}
function hasMultipleContentTypes(platform: string): boolean {
return (contentTypeOptions[platform]?.length || 0) > 1;
}
const getConfig = (postPlatform: PostPlatform): PlatformConfig => {
return props.platformConfigs[postPlatform.social_account_id] || {
maxContentLength: 5000,
maxImages: 10,
allowedMediaTypes: ['image', 'video'],
supportsTextOnly: true,
};
};
// Get content for a platform
const getContent = (platformId: string): string => {
if (synced.value) {
return globalContent.value;
}
return platformContents.value[platformId] || '';
};
// Set content for a platform
const setContent = (platformId: string, value: string) => {
if (synced.value) {
globalContent.value = value;
// Also update all platform contents when synced
for (const id of selectedPlatformIds.value) {
platformContents.value[id] = value;
}
} else {
platformContents.value[platformId] = value;
}
};
// Active platform
const activePlatform = computed(() => {
return props.post.post_platforms.find(pp => pp.id === activeTabId.value);
});
// Current platform media (computed for proper reactivity)
const currentPlatformMedia = computed(() => {
if (!activePlatform.value) return [];
return platformMedia.value[activePlatform.value.id] || [];
});
// Current platform content type
const currentContentType = computed(() => {
if (!activePlatform.value) return '';
return platformContentTypes.value[activePlatform.value.id] || '';
});
// Set content type for a platform
const setContentType = (platformId: string, value: string) => {
platformContentTypes.value[platformId] = value;
};
// Selected platforms (for tabs)
const selectedPlatforms = computed(() => {
return props.post.post_platforms.filter(pp => selectedPlatformIds.value.includes(pp.id));
});
// Unselected platforms (for add button)
const unselectedPlatforms = computed(() => {
return props.post.post_platforms.filter(pp => !selectedPlatformIds.value.includes(pp.id));
});
// Other selected platforms (for sync label)
const otherSelectedPlatforms = computed(() => {
return selectedPlatforms.value.filter(pp => pp.id !== activeTabId.value);
});
// First selected platform (for sync confirmation)
const firstSelectedPlatform = computed(() => {
return selectedPlatforms.value[0];
});
// Handle sync toggle with confirmation
const handleSyncClick = (event: Event) => {
event.preventDefault();
event.stopPropagation();
if (!synced.value) {
// User wants to enable sync - show confirmation dialog
showEnableSyncDialog.value = true;
} else {
// User wants to disable sync - show confirmation dialog
showDisableSyncDialog.value = true;
}
};
// Confirm sync enable
const confirmEnableSync = () => {
if (firstSelectedPlatform.value) {
// Copy content from first platform to global and all platforms
const firstContent = platformContents.value[firstSelectedPlatform.value.id] || '';
globalContent.value = firstContent;
for (const id of selectedPlatformIds.value) {
platformContents.value[id] = firstContent;
}
}
synced.value = true;
showEnableSyncDialog.value = false;
};
// Confirm sync disable
const confirmDisableSync = () => {
synced.value = false;
showDisableSyncDialog.value = false;
};
// Computed
const contentValidation = computed(() => {
const results: Record<string, { valid: boolean; message: string; charCount: number; maxLength: number }> = {};
for (const pp of props.post.post_platforms) {
if (!selectedPlatformIds.value.includes(pp.id)) continue;
const config = getConfig(pp);
const content = getContent(pp.id);
const charCount = content.length;
const hasContent = charCount > 0;
const withinLimit = charCount <= config.maxContentLength;
const media = platformMedia.value[pp.id] || [];
const hasMedia = media.length > 0;
if (!config.supportsTextOnly && !hasMedia) {
results[pp.id] = { valid: false, message: 'Requires media', charCount, maxLength: config.maxContentLength };
} else if (!hasContent && !hasMedia) {
results[pp.id] = { valid: false, message: 'No content', charCount, maxLength: config.maxContentLength };
} else if (!withinLimit) {
results[pp.id] = { valid: false, message: `${charCount - config.maxContentLength} exceeded`, charCount, maxLength: config.maxContentLength };
} else {
results[pp.id] = { valid: true, message: `${charCount}/${config.maxContentLength}`, charCount, maxLength: config.maxContentLength };
}
}
return results;
});
const mediaValidation = computed(() => {
const errors: string[] = [];
for (const pp of props.post.post_platforms) {
if (!selectedPlatformIds.value.includes(pp.id)) continue;
const config = getConfig(pp);
const media = platformMedia.value[pp.id] || [];
const imageCount = media.filter(m => m.type === 'image').length;
const videoCount = media.filter(m => m.type === 'video').length;
if (config.maxImages === 0 && imageCount > 0) {
errors.push(`${getPlatformLabel(pp.platform)} does not support images`);
}
if (imageCount > config.maxImages && config.maxImages > 0) {
errors.push(`${getPlatformLabel(pp.platform)} supports up to ${config.maxImages} images`);
}
if (!config.allowedMediaTypes.includes('video') && videoCount > 0) {
errors.push(`${getPlatformLabel(pp.platform)} does not support videos`);
}
}
return [...new Set(errors)];
});
const canSubmit = computed(() => {
if (selectedPlatformIds.value.length === 0) return false;
if (!scheduledDate.value) return false;
const selectedValidations = Object.entries(contentValidation.value)
.filter(([id]) => selectedPlatformIds.value.includes(id));
return selectedValidations.every(([, v]) => v.valid) && mediaValidation.value.length === 0;
});
// Methods
const togglePlatform = (platformId: string) => {
const index = selectedPlatformIds.value.indexOf(platformId);
if (index === -1) {
selectedPlatformIds.value.push(platformId);
// Switch to newly added platform
activeTabId.value = platformId;
} else {
selectedPlatformIds.value.splice(index, 1);
}
};
// Platforms that only support single media (replace instead of add)
const singleMediaPlatforms = ['tiktok', 'youtube', 'instagram'];
const isSingleMediaPlatform = (platformId: string): boolean => {
const platform = props.post.post_platforms.find(pp => pp.id === platformId);
return platform ? singleMediaPlatforms.includes(platform.platform) : false;
};
const clearPlatformMedia = async (platformId: string) => {
const media = platformMedia.value[platformId] || [];
for (const m of media) {
await axios.delete(destroyMedia.url({ modelId: platformId, media: m.id }));
}
platformMedia.value[platformId] = [];
};
const handleFileUpload = async (event: Event, postPlatformId: string) => {
const input = event.target as HTMLInputElement;
const files = input.files;
if (!files || files.length === 0) return;
// Get other platforms to duplicate to (if synced)
const otherPlatformIds = synced.value
? selectedPlatformIds.value.filter(id => id !== postPlatformId)
: [];
// Mark all as uploading
isUploading.value[postPlatformId] = true;
for (const id of otherPlatformIds) {
isUploading.value[id] = true;
}
// For single-media platforms, clear existing media first
if (isSingleMediaPlatform(postPlatformId)) {
await clearPlatformMedia(postPlatformId);
}
for (const file of Array.from(files)) {
try {
// 1. Upload once to the current platform
const formData = new FormData();
formData.append('media', file);
formData.append('model', 'App\\Models\\PostPlatform');
formData.append('model_id', postPlatformId);
const response = await axios.post(storeMedia.url(), formData);
const data = response.data;
// Add to current platform (use spread for reactivity)
const currentMedia = platformMedia.value[postPlatformId] || [];
platformMedia.value[postPlatformId] = [...currentMedia, data];
// 2. If synced, duplicate to other platforms
if (otherPlatformIds.length > 0) {
const targets = otherPlatformIds.map(id => ({
model: 'App\\Models\\PostPlatform',
model_id: id,
}));
const duplicateResponse = await axios.post(
duplicateMedia.url({ media: data.id }),
{ targets }
);
const duplicates = duplicateResponse.data;
for (const dup of duplicates) {
// For single-media platforms, clear first
if (isSingleMediaPlatform(dup.mediable_id)) {
await clearPlatformMedia(dup.mediable_id);
}
const existingMedia = platformMedia.value[dup.mediable_id] || [];
platformMedia.value[dup.mediable_id] = [...existingMedia, dup];
}
}
} catch (error) {
console.error('Upload failed:', error);
}
}
// Mark all as done
isUploading.value[postPlatformId] = false;
for (const id of otherPlatformIds) {
isUploading.value[id] = false;
}
input.value = '';
};
const removeMedia = async (postPlatformId: string, mediaId: string) => {
// Find the media to get its filename for synced removal
const mediaToRemove = platformMedia.value[postPlatformId]?.find(m => m.id === mediaId);
if (!mediaToRemove) return;
// Get target platforms - all selected if synced, otherwise just the current one
const targetPlatformIds = synced.value ? selectedPlatformIds.value : [postPlatformId];
for (const targetId of targetPlatformIds) {
// Find media with same filename in this platform
const mediaInPlatform = platformMedia.value[targetId]?.find(
m => m.original_filename === mediaToRemove.original_filename
);
if (mediaInPlatform) {
// Remove from local state
platformMedia.value[targetId] = platformMedia.value[targetId].filter(
m => m.id !== mediaInPlatform.id
);
// Delete from server
await axios.delete(destroyMedia.url({ modelId: targetId, media: mediaInPlatform.id }));
}
}
};
const getSubmitData = () => {
const platforms = props.post.post_platforms
.filter(pp => selectedPlatformIds.value.includes(pp.id))
.map(pp => ({
id: pp.id,
content: synced.value ? globalContent.value : platformContents.value[pp.id],
content_type: platformContentTypes.value[pp.id],
}));
// Combine date and time into ISO format
const scheduled_at = scheduledDate.value
? `${scheduledDate.value}T${scheduledTime.value}:00`
: null;
return { platforms, scheduled_at };
};
const save = () => {
if (isSubmitting.value) return;
const { platforms, scheduled_at } = getSubmitData();
isSubmitting.value = true;
router.put(updatePost.url(props.post.id), {
status: props.post.status,
synced: synced.value,
scheduled_at,
platforms,
}, {
preserveScroll: true,
onFinish: () => {
isSubmitting.value = false;
},
});
};
const submit = (status: string = 'scheduled') => {
if (isSubmitting.value) return;
const { platforms, scheduled_at } = getSubmitData();
isSubmitting.value = true;
router.put(updatePost.url(props.post.id), {
status,
synced: synced.value,
scheduled_at,
platforms,
}, {
onFinish: () => {
isSubmitting.value = false;
},
});
};
const deletePost = () => {
deleteModal.value?.open({
url: destroyPost.url(props.post.id),
});
};
</script>
<template>
<Head title="Edit Post" />
<AppLayout :breadcrumbs="breadcrumbs">
<div class="flex flex-col h-[calc(100vh-4rem)]">
<!-- Top Bar with Tabs and Actions -->
<div class="flex items-center justify-between border-b px-4 py-2 bg-background">
<!-- Platform Tabs -->
<div class="flex items-center gap-2">
<Tabs v-model="activeTabId" v-if="selectedPlatforms.length > 0">
<TabsList class="h-auto p-1 gap-1">
<TooltipProvider v-for="pp in selectedPlatforms" :key="pp.id">
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger :value="pp.id" class="relative px-2 py-1.5">
<img
:src="getPlatformLogo(pp.platform)"
:alt="getPlatformLabel(pp.platform)"
class="h-6 w-6 rounded"
/>
<!-- Validation indicator -->
<span
v-if="contentValidation[pp.id]"
class="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full border border-background"
:class="contentValidation[pp.id].valid ? 'bg-green-500' : 'bg-red-500'"
/>
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>
<p>{{ pp.social_account.display_name }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TabsList>
</Tabs>
<!-- Platforms Menu Button -->
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
@click="showPlatformsDialog = true"
class="p-2 rounded-lg hover:bg-muted/50 transition-colors"
>
<MoreHorizontal class="h-5 w-5 text-muted-foreground" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Manage platforms</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span class="mx-2 text-muted-foreground">|</span>
<button
type="button"
@click="deletePost"
class="p-2 rounded-lg hover:bg-muted/50 transition-colors text-muted-foreground hover:text-destructive"
>
<Trash2 class="h-5 w-5" />
</button>
</div>
<!-- Schedule & Actions -->
<div class="flex items-center gap-3">
<!-- Date/Time Picker -->
<div class="flex items-center gap-2">
<DatePicker
name="scheduled_date"
v-model="scheduledDate"
class="w-[140px]"
/>
<Input
type="time"
v-model="scheduledTime"
class="w-[100px] h-9"
/>
<span class="text-xs text-muted-foreground">
{{ timezoneAbbr }}
</span>
</div>
<span class="text-muted-foreground">|</span>
<Button
type="button"
variant="outline"
size="sm"
:disabled="selectedPlatformIds.length === 0 || isSubmitting"
@click="save"
>
{{ isSubmitting ? 'Saving...' : 'Save' }}
</Button>
<Button
type="button"
variant="secondary"
size="sm"
:disabled="!canSubmit || isSubmitting"
@click="submit('scheduled')"
>
<Clock class="mr-2 h-4 w-4" />
Schedule
</Button>
<Button
type="button"
size="sm"
:disabled="!canSubmit || isSubmitting"
@click="submit('publishing')"
>
<Send class="mr-2 h-4 w-4" />
Publish
</Button>
</div>
</div>
<!-- Main Content Area -->
<div class="flex-1 overflow-auto">
<div v-if="activePlatform && selectedPlatformIds.length > 0" class="max-w-2xl mx-auto py-8 px-4">
<!-- Sync Toggle -->
<div v-if="selectedPlatformIds.length > 1" class="flex items-center justify-center gap-2 mb-6">
<div @click.capture="handleSyncClick">
<Switch id="sync-content" v-model="synced" />
</div>
<Label for="sync-content" class="text-sm cursor-pointer flex items-center gap-1.5">
<Link2 class="h-4 w-4" />
<span>Sync with</span>
<span class="flex items-center gap-1">
<img
v-for="op in otherSelectedPlatforms.slice(0, 3)"
:key="op.id"
:src="getPlatformLogo(op.platform)"
class="h-4 w-4 rounded"
/>
<span v-if="otherSelectedPlatforms.length > 3" class="text-muted-foreground">
+{{ otherSelectedPlatforms.length - 3 }}
</span>
</span>
</Label>
</div>
<!-- Content Type Selector -->
<div v-if="hasMultipleContentTypes(activePlatform.platform)" class="flex items-center justify-center gap-2 mb-4">
<Label class="text-sm text-muted-foreground">Post as:</Label>
<Select
:model-value="currentContentType"
@update:model-value="setContentType(activePlatform.id, $event)"
>
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="option in getContentTypeOptions(activePlatform.platform)"
:key="option.value"
:value="option.value"
>
<div class="flex flex-col">
<span>{{ option.label }}</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<!-- Platform Preview -->
<PlatformPreview
:key="activePlatform.id"
:platform="activePlatform.platform"
:social-account="activePlatform.social_account"
:content="getContent(activePlatform.id)"
:media="currentPlatformMedia"
:content-type="currentContentType"
:char-count="contentValidation[activePlatform.id]?.charCount || 0"
:max-length="contentValidation[activePlatform.id]?.maxLength || 5000"
:is-valid="contentValidation[activePlatform.id]?.valid ?? false"
:validation-message="contentValidation[activePlatform.id]?.message || ''"
:is-uploading="isUploading[activePlatform.id]"
@update:content="setContent(activePlatform.id, $event)"
@upload="handleFileUpload($event, activePlatform.id)"
@remove-media="removeMedia(activePlatform.id, $event)"
/>
<!-- Media Validation Errors -->
<Alert v-if="mediaValidation.length > 0" variant="destructive" class="mt-4">
<AlertCircle class="h-4 w-4" />
<AlertDescription>
<ul class="list-disc list-inside">
<li v-for="error in mediaValidation" :key="error">{{ error }}</li>
</ul>
</AlertDescription>
</Alert>
</div>
<!-- Empty State -->
<div v-else class="flex flex-col items-center justify-center h-full text-center p-8">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center mb-4">
<Plus class="h-8 w-8 text-muted-foreground" />
</div>
<h3 class="text-lg font-semibold mb-2">No platforms selected</h3>
<p class="text-muted-foreground mb-4">Select at least one platform to create your post</p>
<div class="flex flex-wrap justify-center gap-2">
<button
v-for="pp in post.post_platforms"
:key="pp.id"
type="button"
@click="togglePlatform(pp.id)"
class="flex items-center gap-2 px-3 py-2 rounded-lg border hover:bg-muted transition-colors"
>
<img :src="getPlatformLogo(pp.platform)" class="h-5 w-5" />
<span class="text-sm">{{ pp.social_account.display_name }}</span>
</button>
</div>
</div>
</div>
</div>
</AppLayout>
<ConfirmDeleteModal
ref="deleteModal"
title="Delete Post"
description="Are you sure you want to delete this post? This action cannot be undone."
action="Delete"
cancel="Cancel"
/>
<!-- Enable Sync Confirmation Dialog -->
<AlertDialog :open="showEnableSyncDialog" @update:open="showEnableSyncDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Discard text and sync with {{ getPlatformLabel(firstSelectedPlatform?.platform || '') }}?
</AlertDialogTitle>
<AlertDialogDescription>
If you enable syncing, you'll lose all edits made specifically to other platforms.
<br /><br />
Are you sure you want to sync with the {{ getPlatformLabel(firstSelectedPlatform?.platform || '') }} version?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="confirmEnableSync">
<Link2 class="mr-2 h-4 w-4" />
Sync with {{ getPlatformLabel(firstSelectedPlatform?.platform || '') }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- Disable Sync Confirmation Dialog -->
<AlertDialog :open="showDisableSyncDialog" @update:open="showDisableSyncDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Disable sync?
</AlertDialogTitle>
<AlertDialogDescription>
Each platform will keep its current content, but future edits will only apply to the platform you're editing.
<br /><br />
You'll be able to customize the content for each platform individually.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="confirmDisableSync">
Disable sync
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- Platforms Selection Dialog -->
<Dialog :open="showPlatformsDialog" @update:open="showPlatformsDialog = $event">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>Select Platforms</DialogTitle>
<DialogDescription>
Choose which platforms to publish this post to.
</DialogDescription>
</DialogHeader>
<div class="space-y-3 py-4">
<div
v-for="pp in post.post_platforms"
:key="pp.id"
class="flex items-center justify-between p-3 rounded-lg border"
>
<div class="flex items-center gap-3">
<div class="relative">
<img
v-if="pp.social_account.avatar_url"
:src="pp.social_account.avatar_url"
:alt="pp.social_account.display_name"
class="h-10 w-10 rounded-full object-cover"
/>
<div v-else class="h-10 w-10 rounded-full bg-muted flex items-center justify-center">
<span class="text-sm font-medium">{{ pp.social_account.display_name?.charAt(0) }}</span>
</div>
<img
:src="getPlatformLogo(pp.platform)"
:alt="pp.platform"
class="absolute -bottom-1 -right-1 h-5 w-5 rounded ring-2 ring-background"
/>
</div>
<div>
<p class="font-medium text-sm">{{ pp.social_account.display_name }}</p>
<p class="text-xs text-muted-foreground">{{ getPlatformLabel(pp.platform) }}</p>
</div>
</div>
<div @click="togglePlatform(pp.id)" class="cursor-pointer">
<Switch :model-value="selectedPlatformIds.includes(pp.id)" />
</div>
</div>
</div>
</DialogContent>
</Dialog>
</template>