feat: added search bar and results page
All checks were successful
Build and Push App Image / build-and-push (push) Successful in 1m34s

This commit is contained in:
domrichardson
2026-03-26 12:52:09 +00:00
parent cf94697d07
commit 9cf71ab4a0
4 changed files with 290 additions and 12 deletions

View File

@@ -117,7 +117,7 @@
</h5>
</div>
<div class="col-auto d-flex align-items-center">
<div v-if="!selectedNote" class="btn-group me-2 d-none d-md-flex" role="group" aria-label="View mode">
<div v-if="!selectedNote || isSearchRoute" class="btn-group me-2 d-none d-md-flex" role="group" aria-label="View mode">
<button
type="button"
class="btn action-button"
@@ -140,7 +140,7 @@
</button>
</div>
<button
v-if="canEditNotes && selectedNote && !isEditingNote"
v-if="canEditNotes && selectedNote && !isEditingNote && !isSearchRoute"
class="btn btn-outline-secondary me-2 action-button"
aria-label="Edit note"
title="Edit note"
@@ -150,7 +150,7 @@
<span class="action-label">Edit Note</span>
</button>
<button
v-if="canShareSelectedNote && !isEditingNote"
v-if="canShareSelectedNote && !isEditingNote && !isSearchRoute"
class="btn btn-outline-primary me-2 action-button"
:aria-label="shareCopied ? 'Link copied' : 'Share note'"
:title="shareCopied ? 'Link copied' : 'Share note'"
@@ -169,8 +169,18 @@
<!-- Note Editor or Note List -->
<div class="content p-4">
<SearchResultsPage
v-if="isSearchRoute"
:notes="searchResults"
:query="searchQuery"
:current-page="searchPage"
:page-size="searchPageSize"
:view-mode="noteViewMode"
@select-note="selectSearchResultNote"
@page-change="setSearchPage"
/>
<NoteEditor
v-if="selectedNote && isEditingNote"
v-else-if="selectedNote && isEditingNote"
:note="selectedNote"
:category-options="categoryOptions"
:can-delete="canDeleteNotes"
@@ -278,6 +288,7 @@ import CategoryTree from "./components/CategoryTree.vue";
import NoteEditor from "./components/NoteEditor.vue";
import NoteViewer from "./components/NoteViewer.vue";
import NoteList from "./components/NoteList.vue";
import SearchResultsPage from "./components/SearchResultsPage.vue";
import CreateSpaceModal from "./components/CreateSpaceModal.vue";
import CreateCategoryModal from "./components/CreateCategoryModal.vue";
import CreateNoteModal from "./components/CreateNoteModal.vue";
@@ -319,10 +330,20 @@ const unlockingNote = ref(false);
const currentUser = computed(() => authStore.user);
const isAdminRoute = computed(() => route.path === "/admin");
const isSearchRoute = computed(() => route.path === "/search");
const isPublicRoute = computed(() => route.path.startsWith("/s/"));
const isAuthRoute = computed(() => route.path === "/login" || route.path === "/register");
const spaces = computed(() => spaceStore.spaces);
const currentSpace = computed(() => spaceStore.currentSpace);
const searchResults = computed(() => sortNotesByPriority(spaceStore.searchResults));
const searchPageSize = 12;
const searchPage = computed(() => {
const pageValue = Number.parseInt(route.query.page || "1", 10);
if (Number.isNaN(pageValue) || pageValue < 1) {
return 1;
}
return pageValue;
});
const categoryTree = computed(() => spaceStore.categoryTree);
const canCreateSpaces = computed(() => authStore.hasPermission("space.create"));
const canCreateCategories = computed(() => authStore.hasSpacePermission(currentSpace.value, "category.create"));
@@ -382,7 +403,7 @@ const canLoadMoreMainNotes = computed(() => {
if (selectedCategory.value || selectedNote.value) {
return false;
}
if (searchQuery.value.trim()) {
if (isSearchRoute.value) {
return false;
}
return spaceStore.notesHasMore;
@@ -411,6 +432,10 @@ const openSpaceHome = () => {
unlockPassword.value = "";
unlockError.value = "";
searchQuery.value = "";
spaceStore.clearSearchResults();
if (route.path !== "/") {
router.push("/");
}
if (currentSpace.value?.id) {
spaceStore.fetchNotes(currentSpace.value.id, { reset: true });
}
@@ -425,6 +450,21 @@ const breadcrumbItems = computed(() => {
return [];
}
if (isSearchRoute.value) {
return [
{
label: currentSpace.value.name,
clickable: true,
onClick: openSpaceHome,
},
{
label: searchQuery.value.trim() ? `Search: ${searchQuery.value.trim()}` : "Search",
clickable: false,
onClick: null,
},
];
}
const items = [
{
label: currentSpace.value.name,
@@ -527,6 +567,30 @@ watch(
},
);
watch(
[() => route.path, () => route.query.q, () => currentSpace.value?.id],
async ([path, routeQuery, spaceId]) => {
if (path !== "/search") {
return;
}
selectedNote.value = null;
selectedCategory.value = null;
isEditingNote.value = false;
const q = typeof routeQuery === "string" ? routeQuery.trim() : "";
searchQuery.value = q;
if (!spaceId || !q) {
spaceStore.clearSearchResults();
return;
}
await spaceStore.searchNotes(q);
},
{ immediate: true },
);
watch(
() => selectedNote.value?.id,
() => {
@@ -683,10 +747,52 @@ const selectCategory = (category) => {
};
const performSearch = async () => {
if (searchQuery.value.trim()) {
await spaceStore.searchNotes(searchQuery.value);
} else if (currentSpace.value?.id) {
await spaceStore.fetchNotes(currentSpace.value.id, { reset: true });
const q = searchQuery.value.trim();
if (!q) {
spaceStore.clearSearchResults();
if (route.path !== "/") {
await router.push("/");
}
if (currentSpace.value?.id) {
await spaceStore.fetchNotes(currentSpace.value.id, { reset: true });
}
return;
}
if (route.path !== "/search" || route.query.q !== q || route.query.page !== "1") {
await router.push({
path: "/search",
query: {
q,
page: "1",
},
});
} else {
await spaceStore.searchNotes(q);
}
};
const setSearchPage = async (page) => {
const q = typeof route.query.q === "string" ? route.query.q : "";
if (!q) {
return;
}
await router.push({
path: "/search",
query: {
q,
page: String(page),
},
});
};
const selectSearchResultNote = async (note) => {
if (!note) {
return;
}
await selectNote(note);
if (route.path === "/search") {
router.push("/");
}
};