feat: search results and queries
This commit is contained in:
parent
3fa8e7f150
commit
292e7df7f8
4 changed files with 122 additions and 27 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import type { ClusterModel, Provider, SearchQuery } from '@/bindings.gen';
|
||||
import type { ClusterModel, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen';
|
||||
import type { UndefinedInitialDataOptions } from '@tanstack/react-query';
|
||||
import { bindings } from '@/main';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface BrowserControllerType {
|
||||
cluster: ClusterModel | undefined;
|
||||
|
|
@ -52,6 +53,7 @@ export function BrowserProvider(props: any) {
|
|||
);
|
||||
}
|
||||
|
||||
export function useBrowserSearch(provider: Provider, query: SearchQuery) {
|
||||
return useCommand('searchPackages', () => bindings.core.searchPackages(provider, query));
|
||||
export function useBrowserSearch(provider: Provider, query: SearchQuery, options?: Omit<UndefinedInitialDataOptions<Paginated<SearchResult>>, 'queryKey' | 'queryFn'> | undefined) {
|
||||
const validFilters = useMemo(() => Object.values(query.filters ?? {}).filter(a => a).length > 0, [query.filters]);
|
||||
return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import type { Provider, SearchResult } from '@/bindings.gen';
|
||||
import OneConfigLogo from '@/assets/logos/oneconfig.svg';
|
||||
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
|
||||
import { abbreviateNumber } from '@/utils';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute, useSearch } from '@tanstack/react-router';
|
||||
import { ChevronRightIcon } from '@untitled-theme/icons-react';
|
||||
import { ChevronRightIcon, Download01Icon, HeartIcon } from '@untitled-theme/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { BrowserLayout } from './route';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/')({
|
||||
|
|
@ -14,6 +17,7 @@ function RouteComponent() {
|
|||
<BrowserLayout>
|
||||
<div className="flex flex-col gap-8">
|
||||
<Featured />
|
||||
<Search />
|
||||
</div>
|
||||
</BrowserLayout>
|
||||
);
|
||||
|
|
@ -21,9 +25,8 @@ function RouteComponent() {
|
|||
|
||||
function Featured() {
|
||||
const context = useBrowserContext();
|
||||
const search = useBrowserSearch(context.provider, context.query);
|
||||
return (
|
||||
<Show when>
|
||||
<Show when={!context.query.query && !(context.query.filters?.categories && context.query.filters.categories.length > 0)}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h5 className="ml-2">Featured</h5>
|
||||
<div className="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
|
||||
|
|
@ -50,13 +53,90 @@ function Featured() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{search.status}
|
||||
{search.isSuccess && (
|
||||
<>
|
||||
{search.data.items.map(item => <p key={item.id}>{item.name}</p>)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
function Search() {
|
||||
const context = useBrowserContext();
|
||||
const search = useBrowserSearch(context.provider, context.query);
|
||||
useEffect(() => {
|
||||
search.refetch();
|
||||
}, [context.provider, context.query]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
|
||||
<Show when={search.isSuccess}>
|
||||
{search.data?.items.map(item => (
|
||||
<PackageItem key={item.project_id} {...item} provider={context.provider} />
|
||||
))}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PackageItem(props: SearchResult & { provider: Provider }) {
|
||||
function redirect() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full min-w-50 flex overflow-hidden rounded-lg bg-component-bg hover:bg-component-bg-hover flex-col max-h-74 min-h-74"
|
||||
onClick={redirect}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="relative flex items-center justify-center overflow-hidden w-full h-28"
|
||||
>
|
||||
<Show
|
||||
fallback={(
|
||||
<div
|
||||
className="aspect-ratio-square rounded-md bg-border/05 w-2/5"
|
||||
/>
|
||||
)}
|
||||
when={props.icon_url}
|
||||
>
|
||||
<img alt={`Icon for ${props.title}`} className="absolute z-0 max-w-none w-7/6 opacity-50 filter-blur-xl" src={props.icon_url} />
|
||||
<img
|
||||
alt={`Icon for ${props.title}`}
|
||||
className="relative z-1 aspect-ratio-square rounded-md image-render-auto w-2/5"
|
||||
src={props.icon_url}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2 p-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-fg-primary font-medium line-height-normal">{props.title}</h4>
|
||||
<p className="text-xs text-fg-secondary">
|
||||
By
|
||||
{' '}
|
||||
<span className="text-fg-primary">{props.author}</span>
|
||||
{' '}
|
||||
on
|
||||
{' '}
|
||||
<span className="text-fg-primary">{props.provider}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="max-h-22 flex-1 overflow-hidden text-sm text-fg-secondary line-height-snug">{props.description}</p>
|
||||
|
||||
<div className="flex flex-row gap-4 text-xs">
|
||||
<Show when={props.provider !== 'SkyClient'}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Download01Icon className="h-4 w-4" />
|
||||
{abbreviateNumber(props.downloads)}
|
||||
</div>
|
||||
|
||||
{/* <Show when={props.follows > 0}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<HeartIcon className="h-4 w-4" />
|
||||
{abbreviateNumber(props.follows)}
|
||||
</div>
|
||||
</Show> */}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { browserCategories } from '@/utils/browser';
|
|||
import { Dropdown, Show, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
import { SearchMdIcon } from '@untitled-theme/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/app/browser')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -51,11 +52,16 @@ export function BrowserLayout(props: any) {
|
|||
function BrowserSidebar() {
|
||||
const context = useBrowserContext();
|
||||
const clusters = useClusters();
|
||||
useEffect(() => {
|
||||
const game_versions = context.cluster?.mc_version ? [context.cluster.mc_version] : null;
|
||||
const loaders = context.cluster?.mc_loader ? [context.cluster.mc_loader] : null;
|
||||
context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, game_versions, loaders } });
|
||||
}, [context.cluster]);
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h6 className="my-1">Active Cluster</h6>
|
||||
<h6 className="my-1 uppercase opacity-60">Active Cluster</h6>
|
||||
<Dropdown
|
||||
onSelectionChange={(id) => {
|
||||
const cluster = clusters?.find(cluster => cluster.id.toString() === id);
|
||||
|
|
@ -71,7 +77,7 @@ function BrowserSidebar() {
|
|||
</Dropdown>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h6 className="my-1">Provider</h6>
|
||||
<h6 className="my-1 uppercase opacity-60">Provider</h6>
|
||||
<Dropdown onSelectionChange={id => context.setProvider(id as typeof context.provider)} selectedKey={context.provider}>
|
||||
{PROVIDERS.map(provider => (
|
||||
<Dropdown.Item id={provider} key={provider}>
|
||||
|
|
@ -88,22 +94,22 @@ function BrowserSidebar() {
|
|||
);
|
||||
}
|
||||
|
||||
const defaultFilters:Filters = {
|
||||
const defaultFilters: Filters = {
|
||||
categories: null,
|
||||
game_versions: null,
|
||||
loaders: null,
|
||||
package_types: null
|
||||
}
|
||||
package_types: null,
|
||||
};
|
||||
|
||||
function BrowserCategories() {
|
||||
const context = useBrowserContext();
|
||||
const categories = browserCategories.byPackageType('mod', context.provider);
|
||||
|
||||
function switchCategory(category:string){
|
||||
if(context.query.filters?.categories?.includes(category))
|
||||
context.setQuery({...context.query, filters: {...defaultFilters, ...context.query.filters, categories: (context.query.filters?.categories ?? []).filter(cat=>cat!==category)}})
|
||||
else
|
||||
context.setQuery({...context.query, filters: {...defaultFilters, ...context.query.filters, categories: [...(context.query.filters?.categories ?? []), category]}})
|
||||
function switchCategory(category: string) {
|
||||
const newCategories = context.query.filters?.categories?.includes(category)
|
||||
? context.query.filters.categories.filter(cat => cat !== category)
|
||||
: [...(context.query.filters?.categories ?? []), category];
|
||||
context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, categories: newCategories.length > 0 ? newCategories : null } });
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -112,13 +118,13 @@ function BrowserCategories() {
|
|||
<div className="flex flex-col gap-y-6">
|
||||
<Show when>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h6 className="my-1 uppercase">Categories</h6>
|
||||
<h6 className="my-1 uppercase opacity-60">Categories</h6>
|
||||
{categories.map(category => (
|
||||
<p
|
||||
aria-selected={context.query.filters?.categories?.includes(category.id)}
|
||||
className="text-md capitalize opacity-60 hover:opacity-90 text-fg-primary hover:text-fg-primary-hover aria-selected:opacity-100"
|
||||
key={category.id}
|
||||
onClick={()=>switchCategory(category.id)}
|
||||
onClick={() => switchCategory(category.id)}
|
||||
>
|
||||
{category.display}
|
||||
</p>
|
||||
|
|
@ -131,6 +137,7 @@ function BrowserCategories() {
|
|||
}
|
||||
|
||||
function BrowserToolbar() {
|
||||
const context = useBrowserContext();
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-between bg-page">
|
||||
<div className="flex flex-row gap-2" />
|
||||
|
|
@ -138,6 +145,12 @@ function BrowserToolbar() {
|
|||
<div className="flex flex-row gap-2">
|
||||
<TextField
|
||||
iconLeft={<SearchMdIcon />}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== 'Enter')
|
||||
return;
|
||||
e.preventDefault();
|
||||
context.setQuery({ ...context.query, query: e.currentTarget.value });
|
||||
}}
|
||||
placeholder="Search for content"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70,11 +70,11 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
|
||||
if let Some(loaders) = &filters.loaders {
|
||||
for loader in loaders {
|
||||
builder.and(Facet("loaders".to_string(), FacetOperation::Eq, loader.to_string()));
|
||||
builder.and(Facet("categories".to_string(), FacetOperation::Eq, loader.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
params.append_pair("facets", &builder.build());
|
||||
params.append_pair("facets", &format!("[{}]", &builder.build()));
|
||||
}
|
||||
|
||||
if let Some(sort) = &query.sort {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue