- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared GalleryBrowser used by both /assets and a new MediaPickerDialog inside the post editor; add JSON search endpoint for workspace assets with tests - emoji: replace broken emoji-picker-element web component with a custom EmojiPicker (full Unicode set, search, categories, recently-used, light/dark, i18n) - preview tab: platform selector pills, variant tabs (data-driven from content_types map) so the user can switch Feed/Reel/Story etc. and have it autosave through the same handler ScheduleTab uses - platform logos: shared usePlatformLogo composable (logo + label + content types); replaces inline maps across 5 components, fixes instagram-facebook falling back to default.png - tooltips: hover details (display_name · @username + platform label) on platform avatars across editor, posts list and calendar - settings cards: show ` · @username` in the title bar so multiple accounts on the same network are distinguishable - routes: drop the throttle:6,1 group middleware on social connect routes (was 429ing legitimate OAuth retries) and rely on the default limiter
113 lines
4 KiB
Vue
113 lines
4 KiB
Vue
<script setup lang="ts">
|
|
import { IconAlertTriangle, IconBrandFacebook, IconChevronDown, IconChevronUp } from '@tabler/icons-vue';
|
|
import { computed, ref } from 'vue';
|
|
|
|
import { Avatar } from '@/components/ui/avatar';
|
|
import { getMediaValidationWarning } from '@/composables/useMedia';
|
|
|
|
interface SocialAccount {
|
|
id: string;
|
|
platform: string;
|
|
display_name: string;
|
|
username: string;
|
|
avatar_url: string | null;
|
|
}
|
|
|
|
interface MediaItem {
|
|
id: string;
|
|
type?: string;
|
|
mime_type?: string;
|
|
}
|
|
|
|
interface Props {
|
|
socialAccount: SocialAccount | null;
|
|
contentType: string;
|
|
media: MediaItem[];
|
|
disabled?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
disabled: false,
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
'update:contentType': [value: string];
|
|
}>();
|
|
|
|
const open = ref(false);
|
|
|
|
const variants = [
|
|
{ value: 'facebook_post', labelKey: 'posts.form.facebook.variant.post' },
|
|
{ value: 'facebook_reel', labelKey: 'posts.form.facebook.variant.reel' },
|
|
{ value: 'facebook_story', labelKey: 'posts.form.facebook.variant.story' },
|
|
];
|
|
|
|
const pickVariant = (value: string) => {
|
|
if (props.disabled) return;
|
|
emit('update:contentType', value);
|
|
};
|
|
|
|
const warning = computed(() => getMediaValidationWarning(props.contentType, props.media));
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rounded-lg border">
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center justify-between p-4 text-sm font-medium"
|
|
@click="open = !open"
|
|
>
|
|
<span class="flex items-center gap-2">
|
|
<IconBrandFacebook class="h-4 w-4" />
|
|
<span>{{ $t('posts.form.facebook.settings') }}</span>
|
|
<span v-if="socialAccount" class="text-muted-foreground">· @{{ socialAccount.username }}</span>
|
|
</span>
|
|
<IconChevronUp v-if="open" class="h-4 w-4 text-muted-foreground" />
|
|
<IconChevronDown v-else class="h-4 w-4 text-muted-foreground" />
|
|
</button>
|
|
|
|
<div v-if="open" class="space-y-5 border-t px-4 pb-4 pt-4">
|
|
<div v-if="socialAccount" class="flex items-center gap-3 rounded-lg bg-muted/50 p-3">
|
|
<Avatar
|
|
:src="socialAccount.avatar_url"
|
|
:name="socialAccount.display_name"
|
|
class="h-9 w-9 shrink-0 rounded-full"
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-xs text-muted-foreground">{{ $t('posts.form.facebook.posting_to') }}</p>
|
|
<p class="truncate text-sm font-medium">
|
|
{{ socialAccount.display_name }}
|
|
<span class="text-muted-foreground">@{{ socialAccount.username }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<p class="text-sm font-medium">{{ $t('posts.form.facebook.variant_label') }}</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
v-for="variant in variants"
|
|
:key="variant.value"
|
|
type="button"
|
|
class="rounded-full border px-3 py-1.5 text-xs transition-colors"
|
|
:class="contentType === variant.value
|
|
? 'border-primary bg-primary/10 text-primary'
|
|
: 'border-border text-muted-foreground hover:text-foreground'"
|
|
:disabled="disabled"
|
|
@click="pickVariant(variant.value)"
|
|
>
|
|
{{ $t(variant.labelKey) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p
|
|
v-if="warning"
|
|
class="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive"
|
|
>
|
|
<IconAlertTriangle class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
{{ $t(`posts.form.warnings.${warning.key}`, warning.params) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|