feat: search page UI (wip)

pull/6775/head
NGPixel 1 year ago
parent 7f4417ee62
commit a806aa3466
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -15,10 +15,18 @@ extend type Query {
): PageVersion ): PageVersion
searchPages( searchPages(
siteId: UUID!
query: String! query: String!
path: String path: String
locale: String locale: [String]
): PageSearchResponse! tags: [String]
editor: String
publishState: PagePublishState
orderBy: PageSearchSort
orderByDirection: OrderByDirection
offset: Int
limit: Int
): PageSearchResponse
pages( pages(
limit: Int limit: Int
@ -28,7 +36,7 @@ extend type Query {
locale: String locale: String
creatorId: Int creatorId: Int
authorId: Int authorId: Int
): [PageListItem!]! ): [PageListItem]
pageById( pageById(
id: UUID! id: UUID!
@ -261,10 +269,13 @@ type PageSearchResponse {
} }
type PageSearchResult { type PageSearchResult {
id: String id: UUID
title: String title: String
description: String description: String
icon: String
path: String path: String
tags: [String]
updatedAt: Date
locale: String locale: String
} }
@ -378,6 +389,12 @@ input PageTocDepthInput {
max: Int! max: Int!
} }
enum PageSearchSort {
relevancy
title
updated
}
enum PageOrderBy { enum PageOrderBy {
CREATED CREATED
ID ID

@ -1752,6 +1752,9 @@
"profile.title": "Profile", "profile.title": "Profile",
"profile.uploadNewAvatar": "Upload New Image", "profile.uploadNewAvatar": "Upload New Image",
"profile.viewPublicProfile": "View Public Profile", "profile.viewPublicProfile": "View Public Profile",
"search.filters": "Filters",
"search.results": "Search Results",
"search.sortBy": "Sort By",
"tags.clearSelection": "Clear Selection", "tags.clearSelection": "Clear Selection",
"tags.currentSelection": "Current Selection", "tags.currentSelection": "Current Selection",
"tags.locale": "Locale", "tags.locale": "Locale",

@ -31,37 +31,34 @@ q-header.bg-header.text-white.site-header(
) )
q-input( q-input(
dark dark
v-model='state.search' v-model='siteStore.search'
standout='bg-white text-dark' standout='bg-white text-dark'
dense dense
rounded rounded
ref='searchField' ref='searchField'
style='width: 100%;' style='width: 100%;'
label='Search...' label='Search...'
@keyup.enter='onSearchEnter'
@focus='state.searchKbdShortcutShown = false'
@blur='state.searchKbdShortcutShown = true'
) )
template(v-slot:prepend) template(v-slot:prepend)
q-icon(name='las la-search') q-icon(name='las la-search')
template(v-slot:append) template(v-slot:append)
q-icon.cursor-pointer( q-badge.q-mr-sm(
name='las la-times' v-if='state.searchKbdShortcutShown'
@click='state.search=``'
v-if='state.search.length > 0'
:color='$q.dark.isActive ? `blue` : `grey-4`'
)
q-badge.q-ml-sm(
label='Ctrl+K' label='Ctrl+K'
color='grey-7' color='grey-7'
outline outline
@click='searchField.focus()' @click='searchField.focus()'
) )
//- q-btn.q-ml-md( q-icon.cursor-pointer(
//- flat name='las la-times'
//- round size='20px'
//- dense @click='siteStore.search=``'
//- icon='las la-tags' v-if='siteStore.search.length > 0'
//- color='grey' color='grey-6'
//- to='/_tags' )
//- )
q-toolbar( q-toolbar(
style='height: 64px;' style='height: 64px;'
dark dark
@ -124,17 +121,18 @@ q-header.bg-header.text-white.site-header(
</template> </template>
<script setup> <script setup>
import AccountMenu from './AccountMenu.vue'
import NewMenu from './PageNewMenu.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useCommonStore } from 'src/stores/common' import { useCommonStore } from 'src/stores/common'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user' import { useUserStore } from 'src/stores/user'
import AccountMenu from 'src/components/AccountMenu.vue'
import NewMenu from 'src/components/PageNewMenu.vue'
// QUASAR // QUASAR
const $q = useQuasar() const $q = useQuasar()
@ -145,6 +143,11 @@ const commonStore = useCommonStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
const userStore = useUserStore() const userStore = useUserStore()
// ROUTER
const router = useRouter()
const route = useRoute()
// I18N // I18N
const { t } = useI18n() const { t } = useI18n()
@ -152,7 +155,7 @@ const { t } = useI18n()
// DATA // DATA
const state = reactive({ const state = reactive({
search: '' searchKbdShortcutShown: true
}) })
const searchField = ref(null) const searchField = ref(null)
@ -172,12 +175,21 @@ function handleKeyPress (ev) {
} }
} }
function onSearchEnter () {
if (!route.path.startsWith('/_search')) {
router.push({ path: '/_search', query: { q: siteStore.search } })
}
}
// MOUNTED // MOUNTED
onMounted(() => { onMounted(() => {
if (process.env.CLIENT) { if (process.env.CLIENT) {
window.addEventListener('keydown', handleKeyPress) window.addEventListener('keydown', handleKeyPress)
} }
if (route.path.startsWith('/_search')) {
searchField.value.focus()
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (process.env.CLIENT) { if (process.env.CLIENT) {

@ -0,0 +1,330 @@
<template lang="pug">
q-layout(view='hHh Lpr lff')
header-nav
q-page-container.layout-search
.layout-search-card
.layout-search-sd
.text-header {{ t('search.sortBy') }}
q-list(dense, padding)
q-item(clickable, active)
q-item-section(side)
q-icon(name='las la-stream', color='primary')
q-item-section
q-item-label Relevance
q-item-section(side)
q-icon(name='mdi-chevron-double-down', size='sm', color='primary')
q-item(clickable)
q-item-section(side)
q-icon(name='las la-heading')
q-item-section Title
q-item(clickable)
q-item-section(side)
q-icon(name='las la-calendar')
q-item-section Last Updated
.text-header {{ t('search.filters') }}
.q-pa-sm
q-input(
outlined
dense
placeholder='Path starting with...'
prefix='/'
v-model='state.filterPath'
)
template(v-slot:prepend)
q-icon(name='las la-caret-square-right', size='xs')
q-input.q-mt-sm(
outlined
dense
placeholder='Tags'
)
template(v-slot:prepend)
q-icon(name='las la-hashtag', size='xs')
q-input.q-mt-sm(
outlined
dense
placeholder='Last updated...'
)
template(v-slot:prepend)
q-icon(name='las la-calendar', size='xs')
q-input.q-mt-sm(
outlined
dense
placeholder='Last edited by...'
)
template(v-slot:prepend)
q-icon(name='las la-user-edit', size='xs')
q-select.q-mt-sm(
outlined
v-model='state.filterLocale'
emit-value
map-options
dense
:aria-label='t(`admin.groups.ruleLocales`)'
:options='siteStore.locales.active'
option-value='code'
option-label='name'
multiple
:display-value='t(`admin.groups.selectedLocales`, { n: state.filterLocale.length > 0 ? state.filterLocale[0].toUpperCase() : state.filterLocale.length }, state.filterLocale.length)'
)
template(v-slot:prepend)
q-icon(name='las la-language', size='xs')
q-select.q-mt-sm(
outlined
v-model='state.filterEditor'
emit-value
map-options
dense
aria-label='Editor'
:options='editors'
)
template(v-slot:prepend)
q-icon(name='las la-pen-nib', size='xs')
q-select.q-mt-sm(
outlined
v-model='state.filterPublishState'
emit-value
map-options
dense
aria-label='Publish State'
:options='publishStates'
)
template(v-slot:prepend)
q-icon(name='las la-traffic-light', size='xs')
q-page(:style-fn='pageStyle')
.text-header.flex
span {{t('search.results')}}
q-space
span.text-caption #[strong 12] results
q-list(separator, padding)
q-item(v-for='item of 12', clickable)
q-item-section(avatar)
q-avatar(color='primary' text-color='white' rounded icon='las la-file-alt')
q-item-section
q-item-label Page ABC def {{ item }}
q-item-label(caption) Lorem ipsum beep boop foo bar
q-item-label(caption) ...Abc def #[span.text-highlight home] efg hig klm...
q-item-section(side)
.flex
q-chip(
v-for='tag of 3'
square
:color='$q.dark.isActive ? `dark-2` : `grey-3`'
:text-color='$q.dark.isActive ? `grey-4` : `grey-8`'
icon='las la-hashtag'
size='sm'
) tag {{ tag }}
.flex
.text-caption.q-mr-sm.text-grey /beep/boop/hello
.text-caption 2023-01-25
q-inner-loading(:showing='state.loading > 0')
main-overlay-dialog
footer-nav
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
import HeaderNav from 'src/components/HeaderNav.vue'
import FooterNav from 'src/components/FooterNav.vue'
import MainOverlayDialog from 'src/components/MainOverlayDialog.vue'
// QUASAR
const $q = useQuasar()
// STORES
const flagsStore = useFlagsStore()
const siteStore = useSiteStore()
const userStore = useUserStore()
// ROUTER
const router = useRouter()
const route = useRoute()
// I18N
const { t } = useI18n()
// META
useMeta({
titleTemplate: title => `${title} - ${t('profile.title')} - Wiki.js`
})
// DATA
const state = reactive({
loading: 0,
filterPath: '',
filterTags: [],
filterLocale: ['en'],
filterEditor: '',
filterPublishState: ''
})
const editors = computed(() => {
return [
{ label: 'Any editor', value: '' },
{ label: 'AsciiDoc', value: 'asciidoc' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'Visual Editor', value: 'wysiwyg' }
]
})
const publishStates = computed(() => {
return [
{ label: 'Any publish state', value: '' },
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Scheduled', value: 'scheduled' }
]
})
// WATCHERS
watch(() => route.query, async (newQueryObj) => {
if (newQueryObj.q) {
siteStore.search = newQueryObj.q
}
}, { immediate: true })
// METHODS
function pageStyle (offset, height) {
return {
'min-height': `${height - 100 - offset}px`
}
}
</script>
<style lang="scss">
.layout-search {
@at-root .body--light & {
background-color: $grey-3;
}
@at-root .body--dark & {
background-color: $dark-6;
}
&:before {
content: '';
height: 200px;
position: fixed;
top: 0;
width: 100%;
background: radial-gradient(ellipse at bottom, $dark-3, $dark-6);
border-bottom: 1px solid #FFF;
@at-root .body--dark & {
border-bottom-color: $dark-3;
}
}
&:after {
content: '';
height: 1px;
position: fixed;
top: 64px;
width: 100%;
background: linear-gradient(to right, transparent 0%, rgba(255,255,255,.1) 50%, transparent 100%);
}
&-card {
position: relative;
width: 90%;
max-width: 1400px;
margin: 50px auto;
box-shadow: $shadow-2;
border-radius: 7px;
display: flex;
align-items: stretch;
height: 100%;
@at-root .body--light & {
background-color: #FFF;
}
@at-root .body--dark & {
background-color: $dark-3;
}
}
&-sd {
flex: 0 0 300px;
border-radius: 8px 0 0 8px;
overflow: hidden;
@at-root .body--light & {
background-color: $grey-1;
border-right: 1px solid rgba($dark-3, .1);
box-shadow: inset -1px 0 0 #FFF;
}
@at-root .body--dark & {
background-color: $dark-4;
border-right: 1px solid rgba(#FFF, .12);
box-shadow: inset -1px 0 0 rgba($dark-6, .5);
}
}
.text-header {
padding: .75rem 1rem;
font-weight: 500;
@at-root .body--light & {
background-color: $grey-1;
border-bottom: 1px solid $grey-3;
}
@at-root .body--dark & {
background-color: $dark-3;
border-bottom: 1px solid $dark-2;
}
}
.text-highlight {
background-color: rgba($yellow-7, .5);
padding: 0 3px;
border-radius: 3px;
}
.q-page {
flex: 1 1;
.text-header:first-child {
border-top-right-radius: 7px;
}
@at-root .body--light & {
border-left: 1px solid #FFF;
}
@at-root .body--dark & {
border-left: 1px solid rgba($dark-6, .75);
}
}
}
body.body--dark {
background-color: $dark-6;
}
.q-footer {
.q-bar {
@at-root .body--light & {
background-color: $grey-3;
color: $grey-7;
}
@at-root .body--dark & {
background-color: $dark-4;
color: rgba(255,255,255,.3);
}
}
}
</style>

@ -32,6 +32,10 @@ const routes = [
{ path: 'groups', component: () => import('src/pages/ProfileGroups.vue') } { path: 'groups', component: () => import('src/pages/ProfileGroups.vue') }
] ]
}, },
{
path: '/_search',
component: () => import('src/pages/Search.vue')
},
{ {
path: '/_admin', path: '/_admin',
component: () => import('layouts/AdminLayout.vue'), component: () => import('layouts/AdminLayout.vue'),

Loading…
Cancel
Save