Skip to content

Commit

Permalink
Search page for community (#970)
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca authored Oct 10, 2024
1 parent 4882f5f commit 9d123a2
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 33 deletions.
2 changes: 1 addition & 1 deletion spx-gui/src/apis/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type ByPage<T> = {
data: T[]
}

export const OwnerAll = '*'
export const ownerAll = '*'

export enum IsPublic {
personal = 0,
Expand Down
14 changes: 9 additions & 5 deletions spx-gui/src/apis/project.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { FileCollection, ByPage, PaginationParams } from './common'
import { client, IsPublic } from './common'
import { client, IsPublic, ownerAll } from './common'
import { ApiException, ApiExceptionCode } from './common/exception'

export { IsPublic }
export { IsPublic, ownerAll }

export enum ProjectDataType {
Sprite = 0,
Expand Down Expand Up @@ -101,12 +101,16 @@ export function deleteProject(owner: string, name: string) {

export type ListProjectParams = PaginationParams & {
isPublic?: IsPublic
/** Name of project owner, `*` indicates projects of all users */
owner?: string
/** Filter projects by name pattern */
keyword?: string
/** Field by which to order the results */
orderBy?: 'cTime' | 'uTime' | 'likeCount' | 'remixCount' | 'recentLikeCount' | 'recentRemixCount'
/** Order in which to sort the results */
sortOrder?: 'asc' | 'desc'
}

/** `owner: ownerAll` indicates that we want to list project of all users */
export const ownerAll = '*'

export async function listProject(params?: ListProjectParams) {
const { total, data } = await (client.get('/projects/list', params) as Promise<
ByPage<ProjectData>
Expand Down
4 changes: 2 additions & 2 deletions spx-gui/src/components/community/CommunityNavbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ import NavbarWrapper from '@/components/navbar/NavbarWrapper.vue'
import NavbarDropdown from '../navbar/NavbarDropdown.vue'
import NavbarNewProjectItem from '@/components/navbar/NavbarNewProjectItem.vue'
import NavbarOpenProjectItem from '@/components/navbar/NavbarOpenProjectItem.vue'
import { getProjectsRoute } from '@/router'
import { getSearchRoute } from '@/router'
const router = useRouter()
const searchInput = ref('')
function handleSearch() {
router.push(getProjectsRoute(searchInput.value))
router.push(getSearchRoute(searchInput.value))
}
</script>

Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/components/project/item/ProjectItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const handleRemove = useMessageHandle(
margin-top: 4px;
display: flex;
gap: 12px;
font-size: 14px;
font-size: 13px;
color: var(--ui-color-grey-700);
.part {
Expand Down
2 changes: 2 additions & 0 deletions spx-gui/src/components/ui/UIPagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
import { computed } from 'vue'
const props = defineProps<{
/** Total page num */
total: number
/** Current page index, start from `1` */
current: number
}>()
const emit = defineEmits<{
Expand Down
2 changes: 2 additions & 0 deletions spx-gui/src/pages/community/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import CommunityNavbar from '@/components/community/CommunityNavbar.vue'
.wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
background-color: var(--ui-color-grey-300);
}
Expand Down
20 changes: 0 additions & 20 deletions spx-gui/src/pages/community/projects.vue

This file was deleted.

235 changes: 235 additions & 0 deletions spx-gui/src/pages/community/search.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<template>
<section class="header">
<CenteredWrapper class="header-content">
<h1 class="title">
<template v-if="titleForResult != null">
<!-- TODO: support vnode as i18n message to simplify such case -->
{{ $t(titleForResult.prefix) }}<span class="keyword">{{ keyword }}</span
>{{ $t(titleForResult.suffix) }}
</template>
<template v-else>
{{ $t(titleForNoResult) }}
</template>
</h1>
<label class="order">
{{
$t({
en: 'Sort by',
zh: '排序方式'
})
}}
<UISelect v-model:value="order">
<UISelectOption :value="Order.RecentlyUpdated">{{
$t({
en: 'Recently Updated',
zh: '最近更新'
})
}}</UISelectOption>
<UISelectOption :value="Order.MostlyLiked">{{
$t({
en: 'Mostly Liked',
zh: '最受喜欢'
})
}}</UISelectOption>
<UISelectOption :value="Order.MostlyRemixed">{{
$t({
en: 'Mostly Remixed',
zh: '改编最多'
})
}}</UISelectOption>
</UISelect>
</label>
</CenteredWrapper>
</section>
<CenteredWrapper class="main">
<UILoading v-if="isLoading" />
<UIError v-else-if="error != null" :retry="refetch">
{{ $t(error.userMessage) }}
</UIError>
<div v-else-if="result?.data.length === 0" class="empty">
<UIEmpty size="large" />
</div>
<template v-else-if="result != null">
<ul class="projects">
<ProjectItem v-for="project in result.data" :key="project.id" :project="project" />
</ul>
<footer v-show="pageTotal > 1" class="pagination-wrapper">
<UIPagination v-model:current="page" :total="pageTotal" />
</footer>
</template>
</CenteredWrapper>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouteQuery } from '@/utils/route'
import { useQuery } from '@/utils/exception'
import { IsPublic, listProject, ownerAll, type ListProjectParams } from '@/apis/project'
import {
UIError,
UILoading,
UISelect,
UISelectOption,
UIPagination,
UIEmpty
} from '@/components/ui'
import CenteredWrapper from '@/components/community/CenteredWrapper.vue'
import ProjectItem from '@/components/project/item/ProjectItem.vue'
const params = useRouteQuery<'q' | 'p' | 'o'>()
const keyword = computed(() => params.get('q') ?? '')
const pageSize = 8 // 2 rows
const defaultPage = 1
const page = computed<number>({
get() {
const pageStr = params.get('p')
if (pageStr == null || pageStr === '') return defaultPage
const page = parseInt(pageStr)
return isNaN(page) ? defaultPage : page
},
set(p) {
params.set('p', p === defaultPage ? null : p.toString())
}
})
enum Order {
RecentlyUpdated = 'update',
MostlyLiked = 'likes',
MostlyRemixed = 'remix'
}
const defaultOrder = Order.RecentlyUpdated
const order = computed<Order>({
get() {
const orderStr = params.get('o')
if (orderStr == null || orderStr === '') return defaultOrder
if (!Object.values(Order).includes(orderStr as Order)) return defaultOrder
return orderStr as Order
},
set(o) {
params.set({
o: o === defaultOrder ? null : o,
p: null
})
}
})
const pageTotal = computed(() => Math.ceil((result.value?.total ?? 0) / pageSize))
const listParams = computed<ListProjectParams>(() => {
const p: ListProjectParams = {
isPublic: IsPublic.public,
owner: ownerAll,
pageSize,
pageIndex: page.value
}
if (keyword.value !== '') p.keyword = keyword.value
switch (order.value) {
case Order.RecentlyUpdated:
p.orderBy = 'uTime'
p.sortOrder = 'desc'
break
case Order.MostlyLiked:
p.orderBy = 'likeCount'
p.sortOrder = 'desc'
break
case Order.MostlyRemixed:
p.orderBy = 'remixCount'
p.sortOrder = 'desc'
break
}
return p
})
const {
data: result,
isLoading,
error,
refetch
} = useQuery(() => listProject(listParams.value), {
en: 'Failed to search projects',
zh: '搜索项目失败'
})
const titleForResult = computed(() => {
if (result.value == null) return null
return {
prefix: {
en: `Found ${result.value?.total} projects for "`,
zh: `找到 ${result.value?.total} 个关于“`
},
suffix: {
en: '"',
zh: '”的项目'
}
}
})
const titleForNoResult = computed(() => ({
en: 'Search projects',
zh: '搜索项目'
}))
</script>
<style lang="scss" scoped>
.header {
flex: 0 0 64px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--ui-color-grey-100);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
.title {
color: var(--ui-color-title);
font-size: 16px;
line-height: 26px;
.keyword {
color: var(--ui-color-primary-main);
}
}
}
.main {
flex: 1 1 0;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.empty {
flex: 1 1 0;
display: flex;
border-radius: var(--ui-border-radius-2);
background: var(--ui-color-grey-100);
}
.projects {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 20px;
}
.pagination-wrapper {
flex: 0 0 56px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--ui-border-radius-2);
border: 1px solid var(--ui-color-grey-400);
background: var(--ui-color-grey-100);
}
</style>
8 changes: 4 additions & 4 deletions spx-gui/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export function getProjectShareRoute(owner: string, name: string) {
return getProjectPageRoute(owner, name)
}

export function getProjectsRoute(keyword: string = '') {
return keyword !== '' ? `/projects?keyword=${encodeURIComponent(keyword)}` : '/projects'
export function getSearchRoute(keyword: string = '') {
return keyword !== '' ? `/search?q=${encodeURIComponent(keyword)}` : '/search'
}

const routes: Array<RouteRecordRaw> = [
Expand All @@ -39,8 +39,8 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/pages/community/explore.vue')
},
{
path: '/projects',
component: () => import('@/pages/community/projects.vue')
path: '/search',
component: () => import('@/pages/community/search.vue')
},
{
path: '/user/:name',
Expand Down
Loading

0 comments on commit 9d123a2

Please sign in to comment.