-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMessageList.vue
More file actions
252 lines (223 loc) · 11.3 KB
/
MessageList.vue
File metadata and controls
252 lines (223 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import NavBar from '../components/NavBar.vue'
import MessageSidebar from '../components/MessageSidebar.vue'
import { useRoute } from 'vue-router'
import { Bell, ShieldAlert, Cpu, Activity, Info, AlertTriangle, ChevronRight, Search, MailOpen, X } from 'lucide-vue-next'
import MessageRightSidebar from '../components/MessageRightSidebar.vue'
import { getMessages, dealMessages, markAllAsRead as apiMarkAllAsRead } from '../services/messages'
import { globalState, invalidateMessages } from '../store'
const route = useRoute()
// Define message categories
const tabs = [
{ id: 'all', label: 'All Messages' },
{ id: 'unread', label: 'Unread', icon: MailOpen },
{ id: 'system', label: 'System', icon: Info },
{ id: 'product', label: 'Product Updates', icon: Cpu },
{ id: 'security', label: 'Security', icon: ShieldAlert },
{ id: 'alert', label: 'Alerts', icon: AlertTriangle },
{ id: 'service', label: 'Service', icon: Activity }
]
const messages = ref<any[]>([])
const loading = ref(false)
const totalCount = ref(0)
const limit = ref(30)
const offset = ref(0)
const searchQuery = ref('')
const currentCategory = computed(() => {
return (route.query.category as string) || 'all'
})
const filteredMessages = computed(() => messages.value)
const hasPrevious = computed(() => offset.value > 0)
const hasNext = computed(() => offset.value + messages.value.length < totalCount.value)
const fetchMessages = async () => {
loading.value = true
try {
const category = currentCategory.value === 'all' || currentCategory.value === 'unread'
? undefined
: currentCategory.value
const isRead = currentCategory.value === 'unread' ? false : undefined
const res = await getMessages(category, limit.value, offset.value, isRead, searchQuery.value || undefined)
const payload = dealMessages(res)
messages.value = payload.results
totalCount.value = payload.count
} catch (e) {
console.error('Failed to fetch messages', e)
} finally {
loading.value = false
}
}
watch(() => route.query.category, () => {
offset.value = 0
searchQuery.value = ''
fetchMessages()
})
const handleSearch = () => {
offset.value = 0
fetchMessages()
}
const clearSearch = () => {
searchQuery.value = ''
offset.value = 0
fetchMessages()
}
onMounted(() => {
fetchMessages()
})
const handleMarkAllAsRead = async () => {
try {
await apiMarkAllAsRead()
messages.value.forEach(m => m.isRead = true)
invalidateMessages()
} catch (e) {
console.error('Failed to mark all as read', e)
}
}
// Refresh when other components signal that message read-state changed.
watch(() => globalState.messagesVersion, () => {
fetchMessages()
})
const goPreviousPage = async () => {
if (!hasPrevious.value) return
offset.value = Math.max(0, offset.value - limit.value)
await fetchMessages()
}
const goNextPage = async () => {
if (!hasNext.value) return
offset.value += limit.value
await fetchMessages()
}
// Helper: get category icon and color
const getCategoryIndicator = (category: string, severity: string) => {
const map: Record<string, any> = {
'system': { icon: Info, bg: 'bg-blue-50', text: 'text-blue-500' },
'security': { icon: ShieldAlert, bg: 'bg-indigo-50', text: 'text-indigo-500' },
'alert': { icon: AlertTriangle, bg: 'bg-rose-50', text: 'text-rose-500' },
'product': { icon: Cpu, bg: 'bg-emerald-50', text: 'text-emerald-500' },
'service': { icon: Activity, bg: 'bg-purple-50', text: 'text-purple-500' }
}
// Use warning color directly for severe errors or warnings
if (severity === 'error') {
return { icon: AlertTriangle, bg: 'bg-rose-50', text: 'text-rose-500' }
}
if (severity === 'warning') {
return { icon: ShieldAlert, bg: 'bg-amber-50', text: 'text-amber-500' }
}
return map[category] || { icon: Bell, bg: 'bg-slate-100', text: 'text-slate-500' }
}
const getCategoryName = (category: string) => {
const found = tabs.find(t => t.id === category)
return found ? found.label : category
}
</script>
<template>
<div class="min-h-screen bg-slate-50 flex flex-col font-sans">
<NavBar />
<main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
<MessageSidebar />
<!-- Main content: message list -->
<section class="flex-1 flex flex-col w-full min-w-0">
<div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden flex flex-col flex-1 h-full min-h-[600px]">
<!-- Header: title and description -->
<div class="px-8 pt-8 pb-6 border-b border-slate-100 flex flex-col md:flex-row md:items-end justify-between gap-4 bg-white">
<div>
<h1 class="text-2xl font-bold text-slate-800 tracking-tight flex items-center gap-3 mb-1">
Inbox
<span v-if="messages.filter(m => !m.isRead).length > 0" class="px-2 py-0.5 bg-primary/10 text-primary text-xs font-bold rounded-full border border-primary/20">
{{ messages.filter(m => !m.isRead).length }} New
</span>
<span v-if="loading" class="text-xs text-slate-400 font-normal">Loading...</span>
</h1>
<p class="text-[13px] text-slate-500">Manage all your notifications and system alerts.</p>
</div>
<!-- Right side: search input -->
<div class="relative w-full md:w-64">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search class="h-4 w-4 text-slate-400" />
</div>
<input
v-model="searchQuery"
type="text"
class="block w-full pl-9 pr-8 py-1.5 border border-slate-200 rounded-lg leading-5 bg-slate-50 hover:bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary sm:text-sm transition-all shadow-sm"
placeholder="Search messages..."
@keyup.enter="handleSearch"
/>
<button v-if="searchQuery" @click="clearSearch" class="absolute inset-y-0 right-0 pr-2.5 flex items-center text-slate-400 hover:text-slate-600">
<X class="h-3.5 w-3.5" />
</button>
</div>
</div>
<!-- Message list -->
<div class="flex-1 flex flex-col overflow-y-auto">
<div v-if="filteredMessages.length === 0" class="flex-1 flex flex-col items-center justify-center p-12 text-center text-slate-500">
<div class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4 border border-slate-100">
<Bell class="w-6 h-6 text-slate-400 opacity-50" />
</div>
<p class="text-base font-medium text-slate-700">No messages found</p>
<p class="text-sm mt-1">You're all caught up in this category.</p>
</div>
<!-- List content -->
<div v-else class="divide-y divide-slate-100/60">
<router-link
v-for="msg in filteredMessages"
:key="msg.id"
:to="`/messages/${msg.id}`"
class="group block transition-colors relative overflow-hidden bg-white hover:bg-slate-50/70 p-6"
>
<div class="flex gap-4 md:gap-5">
<!-- Category icon container -->
<div class="flex-shrink-0 self-start w-11 h-11 rounded-full flex items-center justify-center border-2 border-white shadow-[0_2px_6px_rgba(0,0,0,0.04)]" :class="getCategoryIndicator(msg.category, msg.severity).bg">
<component :is="getCategoryIndicator(msg.category, msg.severity).icon" class="w-5 h-5" :class="getCategoryIndicator(msg.category, msg.severity).text" />
</div>
<!-- Main content area -->
<div class="flex-1 min-w-0 pr-8">
<div class="flex flex-col md:flex-row md:items-center gap-1.5 md:gap-3 mb-1.5">
<h3 class="text-[15px] truncate transition-colors tracking-tight" :class="msg.isRead ? 'text-slate-500 font-medium' : 'text-slate-900 font-bold'">
{{ msg.title }}
</h3>
<span class="hidden md:inline-block px-2 py-0.5 rounded-md bg-slate-100 text-slate-500 text-[10px] font-semibold uppercase tracking-widest border border-slate-200/50">
{{ getCategoryName(msg.category) }}
</span>
</div>
<p class="text-sm leading-relaxed line-clamp-2 md:pr-10 transition-colors" :class="msg.isRead ? 'text-slate-400 font-normal' : 'text-slate-500 font-medium'">
{{ msg.content }}
</p>
<div class="mt-3 flex items-center gap-3 text-[11px] font-medium transition-colors" :class="msg.isRead ? 'text-slate-400/80' : 'text-slate-400'">
<span>{{ msg.date }}</span>
<div class="w-1 h-1 rounded-full bg-slate-300 md:hidden"></div>
<span class="md:hidden text-primary">{{ getCategoryName(msg.category) }}</span>
</div>
</div>
<!-- Detail arrow (shown on hover) -->
<div class="absolute right-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-200">
<ChevronRight class="w-5 h-5 text-slate-400" />
</div>
</div>
</router-link>
</div>
</div>
<!-- Bottom action bar -->
<div class="p-4 border-t border-slate-100 bg-slate-50/50 flex justify-between items-center text-xs text-slate-500 mt-auto">
<div class="flex items-center gap-4">
<span>Showing {{ filteredMessages.length }} of {{ totalCount }} messages</span>
<button class="disabled:text-slate-300" :disabled="!hasPrevious" @click="goPreviousPage">Previous</button>
<button class="disabled:text-slate-300" :disabled="!hasNext" @click="goNextPage">Next</button>
</div>
<router-link to="/messages/preferences" class="font-medium text-primary hover:text-primary-700 transition-colors">Message Preferences</router-link>
</div>
</div>
</section>
<!-- Right sidebar -->
<MessageRightSidebar @mark-all-read="fetchMessages" />
</main>
</div>
</template>
<style scoped>
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>