@@ -36,6 +36,7 @@ import {
3636 StyledInput ,
3737 useDebugLogger ,
3838 useSearch ,
39+ useServerSearch ,
3940 useVIntl ,
4041} from ' @modrinth/ui'
4142import { capitalizeString , cycleValue } from ' @modrinth/utils'
@@ -87,17 +88,17 @@ const handleProjectMouseEnter = (result: Labrinth.Search.v2.ResultSearchProject)
8788 prefetchTimeout .start ()
8889}
8990
90- const _handleServerProjectMouseEnter = (result : Labrinth .Search .v3 .ResultSearchProject ) => {
91+ const handleServerProjectMouseEnter = (result : Labrinth .Search .v3 .ResultSearchProject ) => {
9192 const slug = result .slug || result .project_id
9293
9394 prefetchTimeout = useTimeoutFn (
9495 async () => {
95- queryClient .prefetchQuery (projectQueryOptions .v2 (slug , modrinthClient ))
96- queryClient .prefetchQuery (projectQueryOptions .v3 (slug , modrinthClient ))
96+ queryClient .prefetchQuery (projectQueryOptions .v2 (slug , client ))
97+ queryClient .prefetchQuery (projectQueryOptions .v3 (slug , client ))
9798
9899 const content = result .minecraft_java_server ?.content
99100 if (content ?.kind === ' modpack' && content .version_id ) {
100- queryClient .prefetchQuery (versionQueryOptions .v3 (content .version_id , modrinthClient ))
101+ queryClient .prefetchQuery (versionQueryOptions .v3 (content .version_id , client ))
101102 }
102103 },
103104 HOVER_DURATION_TO_PREFETCH_MS ,
@@ -114,6 +115,8 @@ const currentType = computed(() =>
114115 queryAsStringOrEmpty (route .params .type ).replaceAll (/ ^ \/ | s\/ ? $ / g , ' ' ),
115116)
116117
118+ const isServerType = computed (() => currentType .value === ' server' )
119+
117120const projectType = computed (() => tags .value .projectTypes .find ((x ) => x .id === currentType .value ))
118121const projectTypes = computed (() => (projectType .value ? [projectType .value .id ] : []))
119122
@@ -330,6 +333,36 @@ const {
330333} = useSearch (projectTypes , tags , serverFilters )
331334debug (' useSearch initialized, requestParams:' , requestParams .value )
332335
336+ const {
337+ serverCurrentSortType,
338+ serverCurrentFilters,
339+ serverToggledGroups,
340+ serverSortTypes,
341+ serverFilterTypes,
342+ serverRequestParams,
343+ createServerPageParams,
344+ } = useServerSearch ({ tags , query , maxResults , currentPage })
345+
346+ const effectiveRequestParams = computed (() =>
347+ isServerType .value ? serverRequestParams .value : requestParams .value ,
348+ )
349+ const effectiveSortTypes = computed (() =>
350+ isServerType .value ? (serverSortTypes as readonly SortType []) : sortTypes ,
351+ )
352+ const effectiveCurrentSortType = computed ({
353+ get : () => (isServerType .value ? serverCurrentSortType .value : currentSortType .value ),
354+ set : (v : SortType ) => {
355+ if (isServerType .value ) serverCurrentSortType .value = v
356+ else currentSortType .value = v
357+ },
358+ })
359+ const effectiveCurrentFilters = computed ({
360+ get : () => (isServerType .value ? serverCurrentFilters .value : currentFilters .value ),
361+ set : (v ) => {
362+ if (isServerType .value ) serverCurrentFilters .value = v
363+ else currentFilters .value = v
364+ },
365+ })
333366const selectedFilterTags = computed (() =>
334367 currentFilters .value
335368 .filter (
@@ -458,6 +491,22 @@ async function serverInstall(project: InstallableSearchResult) {
458491 project .installing = false
459492}
460493
494+ function getServerModpackContent(project : Labrinth .Search .v3 .ResultSearchProject ) {
495+ const content = project .minecraft_java_server ?.content
496+ if (content ?.kind === ' modpack' ) {
497+ const { project_name, project_icon, project_id } = content
498+ if (! project_name ) return undefined
499+ return {
500+ name: project_name ,
501+ icon: project_icon ,
502+ onclick:
503+ project_id !== project .project_id ? () => navigateTo (` /project/${project_id } ` ) : undefined ,
504+ showCustomModpackTooltip: project_id === project .project_id ,
505+ }
506+ }
507+ return undefined
508+ }
509+
461510const noLoad = ref (false )
462511const {
463512 data : rawResults,
@@ -466,19 +515,31 @@ const {
466515} = useLazyFetch (
467516 () => {
468517 const config = useRuntimeConfig ()
469- const base = import .meta .server ? config .apiBaseUrl : config .public .apiBaseUrl
518+ let base = import .meta .server ? config .apiBaseUrl : config .public .apiBaseUrl
519+
520+ if (currentType .value === ' server' ) {
521+ base = base .replace (/ \/ v\d \/ / , ' /v3/' ).replace (/ \/ v\d $ / , ' /v3' )
522+ }
470523
471- const url = ` ${base }search${requestParams .value } `
472- debug (' useLazyFetch URL:' , url )
473- return url
524+ return ` ${base }search${effectiveRequestParams .value } `
474525 },
475526 {
476527 headers: computed (() => withLabrinthCanaryHeader ()),
528+
477529 watch: false ,
478- transform : (hits ) => {
479- debug (' useLazyFetch transform, hits:' , (hits as any )?.total_hits )
530+ transform : (
531+ hits : Labrinth .Search .v2 .SearchResults | Labrinth .Search .v3 .SearchResults ,
532+ ): Labrinth .Search .v2 .SearchResults => {
480533 noLoad .value = false
481- return hits as Labrinth .Search .v2 .SearchResults
534+ if (' hits_per_page' in hits ) {
535+ return {
536+ hits: hits .hits as unknown as Labrinth .Search .v2 .ResultSearchProject [],
537+ total_hits: hits .total_hits ,
538+ limit: hits .hits_per_page ,
539+ offset: (hits .page - 1 ) * hits .hits_per_page ,
540+ }
541+ }
542+ return hits
482543 },
483544 },
484545)
@@ -487,9 +548,18 @@ watch(searchLoading, (val) => debug('searchLoading:', val))
487548watch (rawResults , (val ) => debug (' rawResults changed, total_hits:' , val ?.total_hits ))
488549
489550const results = computed (() => rawResults .value )
490- const pageCount = computed (() =>
491- results .value ? Math .ceil (results .value .total_hits / results .value .limit ) : 1 ,
551+ const serverResults = computed (() =>
552+ isServerType .value ? (results .value as Labrinth .Search .v3 .SearchResults | null ) : null ,
553+ )
554+ const projectResults = computed (() =>
555+ isServerType .value ? null : (results .value as Labrinth .Search .v2 .SearchResults | null ),
492556)
557+ const pageCount = computed (() => {
558+ if (! results .value ) return 1
559+ // @ts-expect-error
560+ const perPage = ' limit' in results .value ? results .value .limit : results .value .hits_per_page
561+ return Math .ceil (results .value .total_hits / perPage )
562+ })
493563
494564function scrollToTop(behavior : ScrollBehavior = ' smooth' ) {
495565 window .scrollTo ({ top: 0 , behavior })
@@ -535,14 +605,14 @@ function updateSearchResults(pageNumber: number = 1, resetScroll = true) {
535605
536606 const params = {
537607 ... persistentParams ,
538- ... createPageParams (),
608+ ... ( isServerType . value ? createServerPageParams () : createPageParams () ),
539609 }
540610
541611 router .replace ({ path: route .path , query: params })
542612 }
543613}
544614
545- watch ([currentFilters ], () => {
615+ watch ([effectiveCurrentFilters ], () => {
546616 updateSearchResults (1 , false )
547617})
548618
@@ -734,41 +804,73 @@ useSeoMeta({
734804 @update:model-value =" updateSearchResults()"
735805 />
736806 </div >
737- <SearchSidebarFilter
738- v-for =" filter in filters.filter((f) => f.display !== 'none')"
739- :key =" `filter-${filter.id}`"
740- v-model:selected-filters =" currentFilters"
741- v-model:toggled-groups =" toggledGroups"
742- v-model:overridden-provided-filter-types =" overriddenProvidedFilterTypes"
743- :provided-filters =" serverFilters"
744- :filter-type =" filter"
745- :class ="
746- filtersMenuOpen
747- ? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
748- : 'card-shadow rounded-2xl bg-bg-raised'
749- "
750- button-class =" button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
751- content-class =" mb-4 mx-3"
752- inner-panel-class =" p-1"
753- :open-by-default =" !(currentType === 'shader' && filter.id === 'game_version')"
754- >
755- <template #header >
756- <h3 class =" m-0 text-lg" >{{ filter.formatted_name }}</h3 >
757- </template >
758- <template v-if =" currentType === ' shader' && filter .id === ' game_version' " #prefix >
759- <div class =" mb-4 grid grid-cols-[auto_1fr] gap-2 px-3 text-sm font-medium text-blue" >
760- <InfoIcon class =" mt-1 size-4" />
761- <span > {{ formatMessage(messages.gameVersionShaderMessage) }}</span >
762- </div >
763- </template >
764- <template #locked-game_version >
765- {{ formatMessage(messages.gameVersionProvidedByServer) }}
766- </template >
767- <template #locked-mod_loader >
768- {{ formatMessage(messages.modLoaderProvidedByServer) }}
769- </template >
770- <template #sync-button > {{ formatMessage(messages.syncFilterButton) }}</template >
771- </SearchSidebarFilter >
807+ <template v-if =" isServerType " >
808+ <SearchSidebarFilter
809+ v-for =" filterType in serverFilterTypes.filter((f) => f.options.length > 0)"
810+ :key =" `server-filter-${filterType.id}`"
811+ v-model:selected-filters =" serverCurrentFilters"
812+ v-model:toggled-groups =" serverToggledGroups"
813+ :provided-filters =" []"
814+ :filter-type =" filterType"
815+ :class ="
816+ filtersMenuOpen
817+ ? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
818+ : 'card-shadow rounded-2xl bg-bg-raised'
819+ "
820+ button-class =" button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
821+ content-class =" mb-4 mx-3"
822+ inner-panel-class =" p-1"
823+ :open-by-default ="
824+ ![
825+ 'server_category_minecraft_server_meta',
826+ 'server_category_minecraft_server_community',
827+ 'server_game_version',
828+ 'server_status',
829+ ].includes(filterType.id)
830+ "
831+ >
832+ <template #header >
833+ <h3 class =" m-0 text-lg" >{{ filterType.formatted_name }}</h3 >
834+ </template >
835+ </SearchSidebarFilter >
836+ </template >
837+ <template v-else >
838+ <SearchSidebarFilter
839+ v-for =" filter in filters.filter((f) => f.display !== 'none')"
840+ :key =" `filter-${filter.id}`"
841+ v-model:selected-filters =" currentFilters"
842+ v-model:toggled-groups =" toggledGroups"
843+ v-model:overridden-provided-filter-types =" overriddenProvidedFilterTypes"
844+ :provided-filters =" serverFilters"
845+ :filter-type =" filter"
846+ :class ="
847+ filtersMenuOpen
848+ ? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
849+ : 'card-shadow rounded-2xl bg-bg-raised'
850+ "
851+ button-class =" button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
852+ content-class =" mb-4 mx-3"
853+ inner-panel-class =" p-1"
854+ :open-by-default =" !(currentType === 'shader' && filter.id === 'game_version')"
855+ >
856+ <template #header >
857+ <h3 class =" m-0 text-lg" >{{ filter.formatted_name }}</h3 >
858+ </template >
859+ <template v-if =" currentType === ' shader' && filter .id === ' game_version' " #prefix >
860+ <div class =" mb-4 grid grid-cols-[auto_1fr] gap-2 px-3 text-sm font-medium text-blue" >
861+ <InfoIcon class =" mt-1 size-4" />
862+ <span > {{ formatMessage(messages.gameVersionShaderMessage) }}</span >
863+ </div >
864+ </template >
865+ <template #locked-game_version >
866+ {{ formatMessage(messages.gameVersionProvidedByServer) }}
867+ </template >
868+ <template #locked-mod_loader >
869+ {{ formatMessage(messages.modLoaderProvidedByServer) }}
870+ </template >
871+ <template #sync-button > {{ formatMessage(messages.syncFilterButton) }}</template >
872+ </SearchSidebarFilter >
873+ </template >
772874 </div >
773875 </aside >
774876 <section class =" normal-page__content" >
@@ -788,10 +890,10 @@ useSeoMeta({
788890 <div class =" flex flex-wrap items-center gap-2" >
789891 <DropdownSelect
790892 v-slot =" { selected }"
791- v-model =" currentSortType "
893+ v-model =" effectiveCurrentSortType "
792894 class =" !w-auto flex-grow md:flex-grow-0"
793895 name =" Sort by"
794- :options =" [...sortTypes ]"
896+ :options =" [...effectiveSortTypes ]"
795897 :display-name =" (option?: SortType) => option?.display"
796898 @change =" updateSearchResults()"
797899 >
@@ -837,6 +939,14 @@ useSeoMeta({
837939 />
838940 </div >
839941 <SearchFilterControl
942+ v-if =" isServerType"
943+ v-model:selected-filters =" serverCurrentFilters"
944+ :filters =" serverFilterTypes"
945+ :provided-filters =" []"
946+ :overridden-provided-filter-types =" []"
947+ />
948+ <SearchFilterControl
949+ v-else
840950 v-model:selected-filters =" currentFilters"
841951 :filters =" filters.filter((f) => f.display !== 'none')"
842952 :provided-filters =" serverFilters"
@@ -854,15 +964,43 @@ useSeoMeta({
854964 resultsDisplayMode === 'grid' || resultsDisplayMode === 'gallery' ? 'grid' : 'list'
855965 "
856966 >
857- <template v-for =" result in results ?.hits " :key =" result .project_id " >
967+ <template v-if =" isServerType " >
968+ <ProjectCard
969+ v-for =" result in serverResults?.hits"
970+ :key =" `server-${result.project_id}`"
971+ :link =" `/server/${result.slug ?? result.project_id}`"
972+ :title =" result.name"
973+ :icon-url =" result.icon_url || undefined"
974+ :summary =" result.summary"
975+ :tags =" result.categories"
976+ :server-online-players =" result.minecraft_java_server?.ping?.data?.players_online ?? 0"
977+ :server-region =" result.minecraft_server?.region"
978+ :server-recent-plays =" result.minecraft_java_server?.verified_plays_2w ?? 0"
979+ :server-status-online =" !!result.minecraft_java_server?.ping?.data"
980+ :server-modpack-content =" getServerModpackContent(result)"
981+ is-server-project
982+ exclude-loaders
983+ :color =" result.color ?? undefined"
984+ :banner =" result.featured_gallery ?? undefined"
985+ :layout ="
986+ resultsDisplayMode === 'grid' || resultsDisplayMode === 'gallery' ? 'grid' : 'list'
987+ "
988+ :max-tags =" 2"
989+ @mouseenter =" handleServerProjectMouseEnter(result)"
990+ @mouseleave =" handleProjectHoverEnd"
991+ />
992+ </template >
993+ <template v-else >
858994 <ProjectCard
995+ v-for =" result in projectResults?.hits"
996+ :key =" result.project_id"
859997 :link =" `/${projectType?.id ?? 'project'}/${result.slug ? result.slug : result.project_id}`"
860998 :title =" result.title"
861999 :icon-url =" result.icon_url"
8621000 :author =" { name: result.author, link: `/user/${result.author}` }"
8631001 :date-updated =" result.date_modified"
8641002 :date-published =" result.date_created"
865- :displayed-date =" currentSortType .name === 'newest' ? 'published' : 'updated'"
1003+ :displayed-date =" effectiveCurrentSortType .name === 'newest' ? 'published' : 'updated'"
8661004 :downloads =" result.downloads"
8671005 :summary =" result.description"
8681006 :tags =" result.display_categories"
@@ -875,8 +1013,8 @@ useSeoMeta({
8751013 :environment ="
8761014 ['mod', 'modpack'].includes(currentType)
8771015 ? {
878- clientSide: result.client_side,
879- serverSide: result.server_side,
1016+ clientSide: result.client_side as Labrinth.Projects.v2.Environment ,
1017+ serverSide: result.server_side as Labrinth.Projects.v2.Environment ,
8801018 }
8811019 : undefined
8821020 "
0 commit comments