Skip to content

Commit 8471a7c

Browse files
hakatashiclaude
andcommitted
Enhance topic records page with pagination and search
- Fix pagination buttons that were always disabled (use is-disabled class instead of disabled attribute on anchor tags) - Add pagination controls with previous/next buttons and page numbers - Increase items per page from 20 to 100 (extracted as ITEMS_PER_PAGE constant) - Add search textbox to filter topics by message text or username - Apply Unicode NFKC normalization for better search matching (handles full-width/half-width characters) - Reset pagination to page 1 when sorting or searching changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 527f035 commit 8471a7c

File tree

1 file changed

+149
-14
lines changed

1 file changed

+149
-14
lines changed

app/pages/records/topic.vue

Lines changed: 149 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55

66
<p class="title">sandboxのトピックログ</p>
77

8+
<div class="field">
9+
<div class="control has-icons-left">
10+
<input
11+
v-model="searchQuery"
12+
class="input"
13+
type="text"
14+
placeholder="トピックを検索..."
15+
>
16+
<span class="icon is-left">
17+
<i class="ri-search-line"/>
18+
</span>
19+
</div>
20+
</div>
21+
822
<div class="control is-spaced">
923
並び替え:
1024
<label class="radio">
@@ -48,16 +62,20 @@
4862
<div class="table-container">
4963
<table class="table topics">
5064
<tbody>
51-
<tr v-for="{message, likes, isLiked} in sortedTopics" :key="message.ts">
65+
<tr v-for="{message, likes, isLiked} in paginatedTopics" :key="message.ts">
5266
<td class="topic-text">
53-
<nuxt-link v-if="message.user" :to="`/users/${message.user}`">
67+
<nuxt-link
68+
v-if="message.user"
69+
:to="`/users/${message.user}`"
70+
class="topic-user"
71+
>
5472
<img
5573
class="topic-icon"
5674
:src="getUserIcon(message)"
5775
>
5876
{{getUserName(message)}}
5977
</nuxt-link>
60-
<span v-else>
78+
<span v-else class="topic-user">
6179
<img
6280
class="topic-icon"
6381
:src="getUserIcon(message)"
@@ -88,6 +106,43 @@
88106
</tbody>
89107
</table>
90108
</div>
109+
110+
<nav
111+
v-if="totalPages > 1"
112+
class="pagination is-centered"
113+
role="navigation"
114+
aria-label="pagination"
115+
>
116+
<a
117+
class="pagination-previous"
118+
:class="{'is-disabled': currentPage === 1}"
119+
@click="changePage(currentPage - 1)"
120+
>
121+
前へ
122+
</a>
123+
<a
124+
class="pagination-next"
125+
:class="{'is-disabled': currentPage === totalPages}"
126+
@click="changePage(currentPage + 1)"
127+
>
128+
次へ
129+
</a>
130+
<ul class="pagination-list">
131+
<li v-for="(page, index) in paginationRange" :key="index">
132+
<span v-if="page === '...'" class="pagination-ellipsis">&hellip;</span>
133+
<a
134+
v-else
135+
class="pagination-link"
136+
:class="{'is-current': page === currentPage}"
137+
:aria-label="`ページ ${page}`"
138+
:aria-current="page === currentPage ? 'page' : null"
139+
@click="() => changePage(page)"
140+
>
141+
{{page}}
142+
</a>
143+
</li>
144+
</ul>
145+
</nav>
91146
</div>
92147
</template>
93148

@@ -97,11 +152,15 @@ import get from 'lodash/get';
97152
import sortBy from 'lodash/sortBy';
98153
import {mapActions, mapGetters, mapState} from 'vuex';
99154
155+
const ITEMS_PER_PAGE = 100;
156+
100157
export default {
101158
data() {
102159
return {
103160
isLoading: true,
104161
sortBy: 'timestamp',
162+
currentPage: 1,
163+
searchQuery: '',
105164
};
106165
},
107166
async fetch({store}) {
@@ -124,28 +183,79 @@ export default {
124183
),
125184
}),
126185
...mapGetters('slackInfos', ['getUser']),
186+
filteredTopics() {
187+
if (!this.searchQuery.trim()) {
188+
return this.topicMessages;
189+
}
190+
const query = this.searchQuery.toLowerCase().normalize('NFKC');
191+
return this.topicMessages.filter(({message}) => {
192+
const text = (message.text?.toLowerCase() || '').normalize('NFKC');
193+
const username = this.getUserName(message).toLowerCase().normalize('NFKC');
194+
return text.includes(query) || username.includes(query);
195+
});
196+
},
127197
sortedTopics() {
198+
let sorted;
128199
if (this.sortBy === 'timestamp') {
129-
return sortBy(this.topicMessages, ({message}) => message.ts).reverse();
130-
}
131-
if (this.sortBy === 'username') {
132-
return sortBy(this.topicMessages, [
200+
sorted = sortBy(this.filteredTopics, ({message}) => message.ts).reverse();
201+
} else if (this.sortBy === 'username') {
202+
sorted = sortBy(this.filteredTopics, [
133203
({message}) => this.getUserName(message),
134204
({message}) => -parseFloat(message.ts),
135205
]);
136-
}
137-
if (this.sortBy === 'likes') {
138-
return sortBy(this.topicMessages, [
206+
} else if (this.sortBy === 'likes') {
207+
sorted = sortBy(this.filteredTopics, [
139208
({likes}) => -likes.length,
140209
({message}) => -parseFloat(message.ts),
141210
]);
142-
}
143-
if (this.sortBy === 'random') {
144-
return sortBy(this.topicMessages, [
211+
} else if (this.sortBy === 'random') {
212+
sorted = sortBy(this.filteredTopics, [
145213
({randomSortKey}) => randomSortKey,
146214
]);
215+
} else {
216+
sorted = this.filteredTopics;
217+
}
218+
return sorted;
219+
},
220+
totalPages() {
221+
return Math.ceil(this.sortedTopics.length / ITEMS_PER_PAGE);
222+
},
223+
paginatedTopics() {
224+
const start = (this.currentPage - 1) * ITEMS_PER_PAGE;
225+
const end = start + ITEMS_PER_PAGE;
226+
return this.sortedTopics.slice(start, end);
227+
},
228+
paginationRange() {
229+
const range = [];
230+
const total = this.totalPages;
231+
const current = this.currentPage;
232+
const delta = 2;
233+
234+
for (let i = Math.max(2, current - delta); i <= Math.min(total - 1, current + delta); i++) {
235+
range.push(i);
147236
}
148-
return this.topicMessages;
237+
238+
if (current - delta > 2) {
239+
range.unshift('...');
240+
}
241+
if (current + delta < total - 1) {
242+
range.push('...');
243+
}
244+
245+
range.unshift(1);
246+
if (total > 1) {
247+
range.push(total);
248+
}
249+
250+
return range;
251+
},
252+
},
253+
watch: {
254+
sortBy() {
255+
this.currentPage = 1;
256+
},
257+
searchQuery() {
258+
this.currentPage = 1;
149259
},
150260
},
151261
mounted() {
@@ -157,6 +267,12 @@ export default {
157267
});
158268
},
159269
methods: {
270+
changePage(page) {
271+
if (page >= 1 && page <= this.totalPages) {
272+
this.currentPage = page;
273+
window.scrollTo({top: 0, behavior: 'smooth'});
274+
}
275+
},
160276
...mapActions({
161277
likeTopicMessage: 'slackInfos/likeTopicMessage',
162278
unlikeTopicMessage: 'slackInfos/unlikeTopicMessage',
@@ -202,6 +318,12 @@ export default {
202318
</script>
203319
204320
<style>
321+
.control {
322+
.radio {
323+
margin-inline-start: 0.5em;
324+
}
325+
}
326+
205327
.topics.table td, .topics.table th {
206328
padding-left: 0.25em;
207329
padding-right: 0.25em;
@@ -210,6 +332,10 @@ export default {
210332
.topic-text {
211333
line-break: anywhere;
212334
min-width: 20em;
335+
336+
.topic-user {
337+
margin-inline-end: 0.2em;
338+
}
213339
}
214340
215341
.topic-icon {
@@ -238,5 +364,14 @@ export default {
238364
.topic-like .icon {
239365
vertical-align: bottom;
240366
}
367+
368+
.pagination {
369+
margin-top: 2rem;
370+
margin-bottom: 2rem;
371+
}
372+
373+
.field {
374+
margin-bottom: 1.5rem;
375+
}
241376
</style>
242377

0 commit comments

Comments
 (0)