Skip to content

Commit 6cbe471

Browse files
committed
feat: 💄 revamp posts index page
- Create tabs component and use it in place of previous post list. - Add new `archived.get.is` API endpoint to fetch archived posts - Create new `useArchivedPosts` composable to handle actions and API calls - Update `ProjectCard` title to be clickable → Article page - Update `PostCard` with more feature (e.g. menu item)
1 parent 089f215 commit 6cbe471

8 files changed

Lines changed: 1198 additions & 257 deletions

File tree

‎components/post/PostCard.vue‎

Lines changed: 303 additions & 87 deletions
Large diffs are not rendered by default.

‎components/posts/PostsSection.vue‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@
134134
</UButton>
135135
</div>
136136

137-
<!-- Posts List -->
138-
<div v-else-if="posts.length > 0" :class="listClasses">
139-
<PostItem
137+
<!-- Posts Grid -->
138+
<div v-else-if="posts.length > 0" :class="gridClasses">
139+
<PostCard
140140
v-for="post in displayedPosts"
141141
:key="post.id"
142142
:post="post"
@@ -146,6 +146,7 @@
146146
:show-secondary-tags="showSecondaryTags"
147147
:show-word-count="showWordCount"
148148
:show-draft-badge="showDraftBadge"
149+
:show-archived-badge="true"
149150
:show-status-indicator="showStatusIndicator"
150151
v-bind="itemProps"
151152
@edit="$emit('edit', $event)"
@@ -423,18 +424,21 @@ const contentClasses = computed(() => {
423424
return classes
424425
})
425426
426-
const listClasses = computed(() => {
427-
const classes = ['space-y-4']
428-
427+
const gridClasses = computed(() => {
428+
const classes = ['grid gap-6']
429+
429430
switch (props.variant) {
430431
case 'compact':
431-
classes.push('space-y-2')
432+
classes.push('grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4')
432433
break
433434
case 'detailed':
434-
classes.push('space-y-6')
435+
classes.push('grid-cols-1 md:grid-cols-2 gap-8')
436+
break
437+
default:
438+
classes.push('grid-cols-1 md:grid-cols-2 lg:grid-cols-3')
435439
break
436440
}
437-
441+
438442
return classes
439443
})
440444
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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

Comments
 (0)