feat: added search bar and results page
All checks were successful
Build and Push App Image / build-and-push (push) Successful in 1m34s
All checks were successful
Build and Push App Image / build-and-push (push) Successful in 1m34s
This commit is contained in:
154
frontend/src/components/SearchResultsPage.vue
Normal file
154
frontend/src/components/SearchResultsPage.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<section class="search-results-page">
|
||||
<header class="search-results-header">
|
||||
<h2>Search Results</h2>
|
||||
<p v-if="query" class="search-meta">{{ totalResults }} matches for "{{ query }}"</p>
|
||||
<p v-else class="search-meta">Type in the top bar and press Enter to search notes.</p>
|
||||
</header>
|
||||
|
||||
<div v-if="!query" class="empty-state">
|
||||
<i class="mdi mdi-magnify empty-state-icon" aria-hidden="true"></i>
|
||||
<h3>Start your search</h3>
|
||||
<p>Use a title, content keyword, or tag to find matching notes in the selected space.</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="totalResults === 0" class="empty-state">
|
||||
<i class="mdi mdi-file-search-outline empty-state-icon" aria-hidden="true"></i>
|
||||
<h3>No matching notes</h3>
|
||||
<p>Try different keywords or a shorter phrase.</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<NoteList :notes="paginatedNotes" :view-mode="viewMode" @select-note="emit('select-note', $event)" />
|
||||
|
||||
<nav v-if="totalPages > 1" class="pagination-bar" aria-label="Search result pages">
|
||||
<button class="btn btn-outline-secondary" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)">Previous</button>
|
||||
<span class="page-indicator">Page {{ currentPage }} of {{ totalPages }}</span>
|
||||
<button class="btn btn-outline-secondary" :disabled="currentPage >= totalPages" @click="goToPage(currentPage + 1)">Next</button>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import NoteList from "./NoteList.vue";
|
||||
|
||||
const props = defineProps({
|
||||
query: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
notes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
viewMode: {
|
||||
type: String,
|
||||
default: "grid",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["select-note", "page-change"]);
|
||||
|
||||
const totalResults = computed(() => props.notes.length);
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalResults.value / props.pageSize)));
|
||||
|
||||
const normalizedPage = computed(() => {
|
||||
if (!Number.isFinite(props.currentPage) || props.currentPage < 1) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(props.currentPage, totalPages.value);
|
||||
});
|
||||
|
||||
const paginatedNotes = computed(() => {
|
||||
const start = (normalizedPage.value - 1) * props.pageSize;
|
||||
return props.notes.slice(start, start + props.pageSize);
|
||||
});
|
||||
|
||||
const goToPage = (page) => {
|
||||
if (page < 1 || page > totalPages.value) {
|
||||
return;
|
||||
}
|
||||
emit("page-change", page);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-results-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.search-results-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-results-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #223149;
|
||||
}
|
||||
|
||||
.search-meta {
|
||||
margin: 0.35rem 0 0;
|
||||
color: #5b6f8b;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
margin-top: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.page-indicator {
|
||||
color: #4f637d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 48vh;
|
||||
border: 1px dashed #cfdae9;
|
||||
border-radius: 14px;
|
||||
background: radial-gradient(circle at 20% 20%, #f2f9ff 0%, #edf2ff 70%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4.2rem;
|
||||
color: #60789a;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0;
|
||||
color: #223149;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0.6rem 0 0;
|
||||
color: #5b6f8b;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pagination-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user