|
| 1 | +<template> |
| 2 | + <div class="w-full"> |
| 3 | + <!-- Header with Actions --> |
| 4 | + <div class="flex items-center justify-between mb-6"> |
| 5 | + <div class="flex items-center gap-3"> |
| 6 | + <h2 class="text-xl font-600 text-gray-800 dark:text-gray-200"> |
| 7 | + {{ title }} |
| 8 | + </h2> |
| 9 | + <UBadge |
| 10 | + v-if="posts.length > 0" |
| 11 | + variant="soft" |
| 12 | + color="gray" |
| 13 | + size="sm" |
| 14 | + > |
| 15 | + {{ posts.length }} |
| 16 | + </UBadge> |
| 17 | + </div> |
| 18 | + |
| 19 | + <slot name="actions" :posts="posts" :is-loading="isLoading" /> |
| 20 | + </div> |
| 21 | + |
| 22 | + <!-- Loading State --> |
| 23 | + <div v-if="isLoading" class="flex items-center justify-center py-12"> |
| 24 | + <div class="flex items-center gap-3 text-gray-600 dark:text-gray-400"> |
| 25 | + <UIcon name="i-lucide-loader-2" class="animate-spin" /> |
| 26 | + <span>Loading posts...</span> |
| 27 | + </div> |
| 28 | + </div> |
| 29 | + |
| 30 | + <!-- Error State --> |
| 31 | + <div v-else-if="error" class="text-center py-12"> |
| 32 | + <div class="flex flex-col items-center gap-4"> |
| 33 | + <UIcon name="i-lucide-alert-circle" class="text-red-500 text-2xl" /> |
| 34 | + <div class="text-red-600 dark:text-red-400"> |
| 35 | + {{ error }} |
| 36 | + </div> |
| 37 | + <UButton |
| 38 | + btn="soft-red" |
| 39 | + size="sm" |
| 40 | + @click="$emit('retry')" |
| 41 | + > |
| 42 | + <UIcon name="i-lucide-refresh-cw" /> |
| 43 | + <span>Try Again</span> |
| 44 | + </UButton> |
| 45 | + </div> |
| 46 | + </div> |
| 47 | + |
| 48 | + <!-- Posts Grid --> |
| 49 | + <div v-else-if="posts.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
| 50 | + <PostCard |
| 51 | + v-for="post in posts" |
| 52 | + :key="post.id" |
| 53 | + :post="post" |
| 54 | + :show-menu="true" |
| 55 | + :show-primary-tag="true" |
| 56 | + :show-secondary-tags="true" |
| 57 | + :show-word-count="true" |
| 58 | + :show-draft-badge="true" |
| 59 | + :show-archived-badge="true" |
| 60 | + :show-status-indicator="false" |
| 61 | + :menu-variant="'minimal'" |
| 62 | + @edit="$emit('edit', $event)" |
| 63 | + @delete="$emit('delete', $event)" |
| 64 | + @publish="$emit('publish', $event)" |
| 65 | + @unpublish="$emit('unpublish', $event)" |
| 66 | + @duplicate="$emit('duplicate', $event)" |
| 67 | + @archive="$emit('archive', $event)" |
| 68 | + @unarchive="$emit('unarchive', $event)" |
| 69 | + @share="$emit('share', $event)" |
| 70 | + @export="$emit('export', $event)" |
| 71 | + @view-stats="$emit('view-stats', $event)" |
| 72 | + /> |
| 73 | + </div> |
| 74 | + |
| 75 | + <!-- Empty State --> |
| 76 | + <div v-else class="text-center py-16"> |
| 77 | + <div class="flex flex-col items-center gap-6 max-w-md mx-auto"> |
| 78 | + <!-- Empty State Icon --> |
| 79 | + <div class="w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center"> |
| 80 | + <UIcon :name="emptyActionIcon" class="text-2xl text-gray-400" /> |
| 81 | + </div> |
| 82 | + |
| 83 | + <!-- Empty State Content --> |
| 84 | + <div class="text-center"> |
| 85 | + <h3 class="text-lg font-600 text-gray-800 dark:text-gray-200 mb-2"> |
| 86 | + {{ emptyTitle }} |
| 87 | + </h3> |
| 88 | + <p class="text-gray-600 dark:text-gray-400 text-sm leading-relaxed"> |
| 89 | + {{ emptyDescription }} |
| 90 | + </p> |
| 91 | + </div> |
| 92 | + |
| 93 | + <!-- Empty State Action --> |
| 94 | + <UButton |
| 95 | + btn="soft-primary" |
| 96 | + size="sm" |
| 97 | + @click="$emit('empty-action')" |
| 98 | + > |
| 99 | + <UIcon :name="emptyActionIcon" /> |
| 100 | + <span>{{ emptyActionText }}</span> |
| 101 | + </UButton> |
| 102 | + </div> |
| 103 | + </div> |
| 104 | + |
| 105 | + <!-- Footer Slot --> |
| 106 | + <div v-if="$slots.footer && posts.length > 0" class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700"> |
| 107 | + <slot name="footer" :posts="posts" :total-count="posts.length" /> |
| 108 | + </div> |
| 109 | + </div> |
| 110 | +</template> |
| 111 | + |
| 112 | +<script setup lang="ts"> |
| 113 | +import type { Post } from '~/types/post' |
| 114 | +
|
| 115 | +interface PostsTabContentProps { |
| 116 | + posts: Post[] |
| 117 | + isLoading: boolean |
| 118 | + error: string | null |
| 119 | + title: string |
| 120 | + emptyTitle: string |
| 121 | + emptyDescription: string |
| 122 | + emptyActionText: string |
| 123 | + emptyActionIcon: string |
| 124 | +} |
| 125 | +
|
| 126 | +interface PostsTabContentEmits { |
| 127 | + // Post actions |
| 128 | + (e: 'edit', post: Post): void |
| 129 | + (e: 'delete', post: Post): void |
| 130 | + (e: 'publish', post: Post): void |
| 131 | + (e: 'unpublish', post: Post): void |
| 132 | + (e: 'archive', post: Post): void |
| 133 | + (e: 'unarchive', post: Post): void |
| 134 | + (e: 'duplicate', post: Post): void |
| 135 | + (e: 'share', post: Post): void |
| 136 | + (e: 'export', post: Post): void |
| 137 | + (e: 'view-stats', post: Post): void |
| 138 | + |
| 139 | + // Control actions |
| 140 | + (e: 'refresh'): void |
| 141 | + (e: 'retry'): void |
| 142 | + (e: 'empty-action'): void |
| 143 | +} |
| 144 | +
|
| 145 | +defineProps<PostsTabContentProps>() |
| 146 | +defineEmits<PostsTabContentEmits>() |
| 147 | +</script> |
0 commit comments