mirror of
https://github.com/arthyn/sphinx.git
synced 2024-12-25 08:52:32 +03:00
adding tag and all browsing
This commit is contained in:
parent
5e4bbd8a2e
commit
4f280c97fe
@ -207,6 +207,31 @@
|
||||
::
|
||||
[%x %state ~]
|
||||
``noun+!>(state)
|
||||
[%x %tags ~]
|
||||
=- ``tags+!>(-)
|
||||
%+ roll
|
||||
~(tap by directory)
|
||||
|= [[=hash:s =listing:s] tags=(map @t @ud)]
|
||||
%-
|
||||
%- ~(uno by tags)
|
||||
%- malt
|
||||
%+ turn
|
||||
tags.post.listing
|
||||
|=(tag=@t [tag 1])
|
||||
|= [key=@t a=@ud b=@ud]
|
||||
(add a b)
|
||||
::
|
||||
[%x %lookup %tag @ @ @ ~]
|
||||
=- ``search+!>(-)
|
||||
^- search:s
|
||||
=/ start (slav %ud i.t.t.t.path)
|
||||
=/ limit (slav %ud i.t.t.t.t.path)
|
||||
=/ tag (slav %t i.t.t.t.t.t.path)
|
||||
%^ page start limit
|
||||
%+ skim
|
||||
~(val by directory)
|
||||
|= =listing:s
|
||||
!=(~ (find ~[tag] tags.post.listing))
|
||||
::
|
||||
[%x %lookup @ @ @ $@(~ [@ ~])]
|
||||
=- ``search+!>(-)
|
||||
@ -221,31 +246,33 @@
|
||||
`@t`(slav %t i.t.t.t.t.t.path)
|
||||
~
|
||||
:: expects encoded @t values)
|
||||
=/ all
|
||||
?~ term
|
||||
%+ skim
|
||||
~(val by directory)
|
||||
|= =listing:s
|
||||
|(=(filter %all) =(filter type.post.listing))
|
||||
=/ entries (~(get-entries delver index) term)
|
||||
:: ~& %+ turn
|
||||
:: entries
|
||||
:: |= [=hash:s =rank:s]
|
||||
:: [(~(got by directory) hash) rank]
|
||||
%^ page start limit
|
||||
?~ term
|
||||
%+ skim
|
||||
%- get-listings
|
||||
%- get-hashes
|
||||
%- sort-entries
|
||||
entries
|
||||
~(val by directory)
|
||||
|= =listing:s
|
||||
|(=(filter %all) =(filter type.post.listing))
|
||||
=/ listings (swag [start limit] all)
|
||||
:* listings
|
||||
start
|
||||
limit
|
||||
(lent listings)
|
||||
(lent all)
|
||||
==
|
||||
=/ entries (~(get-entries delver index) term)
|
||||
:: ~& %+ turn
|
||||
:: entries
|
||||
:: |= [=hash:s =rank:s]
|
||||
:: [(~(got by directory) hash) rank]
|
||||
%+ skim
|
||||
%- get-listings
|
||||
%- get-hashes
|
||||
%- sort-entries
|
||||
entries
|
||||
|= =listing:s
|
||||
|(=(filter %all) =(filter type.post.listing))
|
||||
==
|
||||
++ page
|
||||
|= [start=@ud limit=@ud all=(list listing:s)]
|
||||
=/ listings (swag [start limit] all)
|
||||
:* listings
|
||||
start
|
||||
limit
|
||||
(lent listings)
|
||||
(lent all)
|
||||
==
|
||||
++ get-listings
|
||||
|= l=(list hash)
|
||||
@ -287,7 +314,7 @@
|
||||
=. listing l
|
||||
=. index (~(catalog delver index) hash l)
|
||||
=. published
|
||||
?. =(src our):bowl published
|
||||
?. =(source.listing our):bowl published
|
||||
(~(put by directory) hash.listing listing)
|
||||
=. cor (emit (invent:gossip %directory-listing !>(listing)))
|
||||
di-core
|
||||
|
@ -37,6 +37,12 @@
|
||||
%+ turn ~(tap by d)
|
||||
|= [=hash:s l=listing:s]
|
||||
[(scot %uv hash) (listing l)]
|
||||
++ tags
|
||||
|= tags=(map @t @ud)
|
||||
%- pairs
|
||||
%+ turn ~(tap by tags)
|
||||
|= [tag=@t count=@ud]
|
||||
[tag (numb count)]
|
||||
--
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|
@ -9,6 +9,6 @@
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun search
|
||||
++ noun search:s
|
||||
--
|
||||
--
|
||||
|
14
desk/mar/tags.hoon
Normal file
14
desk/mar/tags.hoon
Normal file
@ -0,0 +1,14 @@
|
||||
/- s=sphinx
|
||||
/+ j=sphinx-json
|
||||
|_ tags=(map @t @ud)
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun tags
|
||||
++ json (tags:enjs:j tags)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun (map @t @ud)
|
||||
--
|
||||
--
|
@ -2,11 +2,13 @@ import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Layout } from './components/Layout';
|
||||
import { AllListings } from './manage-listings/AllListings';
|
||||
import { Apps } from './manage-listings/Apps';
|
||||
import { Groups } from './manage-listings/Groups';
|
||||
import { MyListings } from './manage-listings/MyListings';
|
||||
import { Post } from './manage-listings/Post';
|
||||
import { Search } from './pages/Search';
|
||||
import { Tag } from './pages/Tag';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@ -18,8 +20,13 @@ function Main() {
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route path="/search/:lookup" element={<Search />} />
|
||||
<Route path="/search/:lookup/:limit/:page" element={<Search />} />
|
||||
<Route path="/tags" element={<Tag />} />
|
||||
<Route path="/tags/:tag" element={<Tag />} />
|
||||
<Route path="/tags/:tag/:limit/:page" element={<Tag />} />
|
||||
<Route path="/manage-listings">
|
||||
<Route index element={<MyListings />} />
|
||||
<Route path="all" element={<AllListings />} />
|
||||
<Route path="all/:limit/:page" element={<AllListings />} />
|
||||
<Route path="post" element={<Post />} />
|
||||
<Route path="apps" element={<Apps />} />
|
||||
<Route path="groups" element={<Groups />} />
|
||||
|
@ -1,14 +1,12 @@
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { NavLink, Outlet, useParams } from 'react-router-dom';
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { Meta } from './Meta';
|
||||
|
||||
export const Layout = () => {
|
||||
const params = useParams<{ lookup: string }>();
|
||||
|
||||
return (
|
||||
<main className={cn("flex flex-col items-center min-h-screen", !params.lookup && 'justify-center')}>
|
||||
<div className="max-w-2xl w-full p-4 sm:py-12 sm:px-8 space-y-6">
|
||||
<main className={cn("flex flex-col items-center h-full min-h-screen")}>
|
||||
<div className="flex-1 flex flex-col max-w-2xl h-full w-full p-4 sm:py-12 sm:px-8 space-y-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
<aside className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4'>
|
||||
@ -17,9 +15,15 @@ export const Layout = () => {
|
||||
<li>
|
||||
<NavLink to="/search" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>search</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/tags" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>tags</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/manage-listings" end className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>my listings</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/manage-listings/all" end className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>all listings</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/manage-listings/post" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>post listing</NavLink>
|
||||
</li>
|
||||
|
18
ui/src/components/TagCloud.tsx
Normal file
18
ui/src/components/TagCloud.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
interface TagCloudProps {
|
||||
tags: [string, number][];
|
||||
}
|
||||
|
||||
export const TagCloud = ({ tags }: TagCloudProps) => (
|
||||
<section className='flex flex-wrap gap-2'>
|
||||
{tags.map(([t, count]) => (
|
||||
<NavLink key={t} to={`/tags/${t}`} className={({ isActive }) => cn("py-0.5 px-1.5 space-x-2 rounded-md font-semibold", isActive ? 'bg-lavender text-linen' : 'bg-fawn')}>
|
||||
<span>{t}</span>
|
||||
<span className='opacity-60'>{count}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</section>
|
||||
)
|
63
ui/src/manage-listings/AllListings.tsx
Normal file
63
ui/src/manage-listings/AllListings.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { Listings } from '../components/Listings';
|
||||
import { Paginator } from '../components/Paginator';
|
||||
import { useSearch } from '../state/search';
|
||||
import { Remove, Search } from '../types/sphinx';
|
||||
|
||||
interface RouteParams extends Record<string, string | undefined> {
|
||||
limit?: string;
|
||||
page?: string;
|
||||
}
|
||||
|
||||
export const AllListings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
limit,
|
||||
page
|
||||
} = useParams<RouteParams>();
|
||||
const {
|
||||
results,
|
||||
pages,
|
||||
pageInt,
|
||||
linkBuild
|
||||
} = useSearch({
|
||||
key: (start, size) => `all-${start}-${size}`,
|
||||
fetcher: (start, size) => api.scry<Search>({
|
||||
app: 'sphinx',
|
||||
path: `/lookup/all/${start}/${size}`
|
||||
}),
|
||||
enabled: true,
|
||||
limit,
|
||||
page,
|
||||
linkPrefix: '/manage-listings/all'
|
||||
})
|
||||
const { mutate } = useMutation((hash: string) => {
|
||||
return api.poke<Remove>({
|
||||
app: 'sphinx',
|
||||
mark: 'remove',
|
||||
json: hash
|
||||
})
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('published');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='flex items-center'>
|
||||
<h1 className='text-2xl font-semibold'>All Listings</h1>
|
||||
</header>
|
||||
{results && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
<Listings listings={results.listings} remove={mutate} />
|
||||
{results && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
}
|
@ -52,7 +52,7 @@ export const Apps = () => {
|
||||
}, [reset, mutate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full space-y-6 m-auto'>
|
||||
<header className='flex items-center'>
|
||||
<h1 className='text-2xl font-semibold'>Add Apps</h1>
|
||||
{apps.length > 0 && (
|
||||
@ -86,6 +86,6 @@ export const Apps = () => {
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -59,7 +59,7 @@ export const Groups = () => {
|
||||
}, [reset, mutate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full space-y-6 m-auto'>
|
||||
<header className='flex items-center'>
|
||||
<h1 className='text-2xl font-semibold'>Add Groups</h1>
|
||||
{groups.length > 0 && (
|
||||
@ -93,6 +93,6 @@ export const Groups = () => {
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -24,11 +24,11 @@ export const MyListings = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full space-y-6 m-auto'>
|
||||
<header className='flex items-center'>
|
||||
<h1 className='text-2xl font-semibold'>My Listings</h1>
|
||||
</header>
|
||||
<Listings listings={Object.values(data || {})} remove={mutate} className="mt-6" />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -62,7 +62,7 @@ export const Post = () => {
|
||||
}, [img]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full space-y-6 m-auto'>
|
||||
<header>
|
||||
<h1 className='text-2xl font-semibold'>Add a Listing</h1>
|
||||
</header>
|
||||
@ -119,6 +119,6 @@ export const Post = () => {
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import cn from 'classnames';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { stringToTa } from '@urbit/api';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
import { Listings } from '../components/Listings';
|
||||
import { PostFilter, Remove, Search as SearchType } from '../types/sphinx';
|
||||
@ -12,6 +11,10 @@ import { Paginator } from '../components/Paginator';
|
||||
import { Filter } from '../components/Filter';
|
||||
import { PlusSmIcon } from '@heroicons/react/solid';
|
||||
import { usePals } from '../state/pals';
|
||||
import { TagCloud } from '../components/TagCloud';
|
||||
import { useTags } from '../state/tags';
|
||||
import { useSearch } from '../state/search';
|
||||
import { encodeLookup } from '../utils';
|
||||
|
||||
interface RouteParams extends Record<string, string | undefined> {
|
||||
lookup?: string;
|
||||
@ -19,14 +22,6 @@ interface RouteParams extends Record<string, string | undefined> {
|
||||
page?: string;
|
||||
}
|
||||
|
||||
function encodeLookup(value: string | undefined) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return stringToTa(value).replace('~.', '~~');
|
||||
}
|
||||
|
||||
export const Search = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
@ -37,16 +32,26 @@ export const Search = () => {
|
||||
const [selected, setSelected] = useState<PostFilter>('all')
|
||||
const [rawSearch, setRawSearch] = useState(lookup || '');
|
||||
const { installed: palsInstalled } = usePals();
|
||||
const size = parseInt(limit || '10', 10);
|
||||
const pageInt = parseInt(page || '1', 10) - 1;
|
||||
const start = pageInt * size;
|
||||
const { data } = useQuery<unknown, unknown, SearchType>(`lookup-${selected}-${size}-${start}-${lookup}`, () => api.scry<SearchType>({
|
||||
app: 'sphinx',
|
||||
path: `/lookup/${selected}/${start}/${size}/${encodeLookup(lookup)}`
|
||||
}), {
|
||||
|
||||
const {
|
||||
size,
|
||||
start,
|
||||
pageInt,
|
||||
pages,
|
||||
results,
|
||||
linkBuild
|
||||
} = useSearch({
|
||||
key: (start, size) => `lookup-${selected}-${size}-${start}-${lookup}`,
|
||||
fetcher: (start, size) => api.scry<SearchType>({
|
||||
app: 'sphinx',
|
||||
path: `/lookup/${selected}/${start}/${size}/${encodeLookup(lookup)}`
|
||||
}),
|
||||
enabled: !!lookup,
|
||||
keepPreviousData: true
|
||||
limit,
|
||||
page,
|
||||
linkPrefix: `/search/${lookup}`
|
||||
});
|
||||
const tags = useTags();
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate } = useMutation((hash: string) => {
|
||||
return api.poke<Remove>({
|
||||
@ -60,12 +65,6 @@ export const Search = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const total = data?.total || 0;
|
||||
const pages =
|
||||
total % size === 0
|
||||
? total / size
|
||||
: Math.floor(total / size) + 1;
|
||||
|
||||
const update = useRef(debounce((value: string) => {
|
||||
if (!value) {
|
||||
return;
|
||||
@ -79,16 +78,8 @@ export const Search = () => {
|
||||
update.current(value);
|
||||
}, []);
|
||||
|
||||
const linkBuild = useCallback((page) => {
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `/search/${lookup}/${size}/${page || 1}`
|
||||
}, [lookup, size]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('w-full space-y-6', !lookup && 'm-auto')}>
|
||||
<header className='flex items-center space-x-2'>
|
||||
<SearchInput className='flex-1' lookup={rawSearch} onChange={onChange} />
|
||||
<Filter selected={selected} onSelect={setSelected} className="min-w-0 sm:w-20" />
|
||||
@ -102,13 +93,19 @@ export const Search = () => {
|
||||
to see listings from others
|
||||
</div>
|
||||
)}
|
||||
{lookup && data && <div className='flex justify-end border-t border-zinc-300'>
|
||||
{lookup && results && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
{lookup && <Listings listings={data?.listings || []} remove={mutate} />}
|
||||
{data && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
|
||||
{lookup && <Listings listings={results.listings} remove={mutate} />}
|
||||
{!lookup && (
|
||||
<div className='space-y-2'>
|
||||
<h2 className='font-semibold'>top tags</h2>
|
||||
<TagCloud tags={tags.slice(0,12)} />
|
||||
</div>
|
||||
)}
|
||||
{results && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
74
ui/src/pages/Tag.tsx
Normal file
74
ui/src/pages/Tag.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { useQueryClient, useMutation } from 'react-query';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { Listings } from '../components/Listings';
|
||||
import { Paginator } from '../components/Paginator';
|
||||
import { TagCloud } from '../components/TagCloud';
|
||||
import { useSearch } from '../state/search';
|
||||
import { useTags } from '../state/tags';
|
||||
import { Remove, Search } from '../types/sphinx';
|
||||
import { encodeLookup } from '../utils';
|
||||
|
||||
interface RouteParams extends Record<string, string | undefined> {
|
||||
tag?: string;
|
||||
limit?: string;
|
||||
page?: string;
|
||||
}
|
||||
|
||||
const tagKey = (tag?: string) => (start: number, size: number) => `tag-${tag || ''}-${size}-${start}`
|
||||
|
||||
export const Tag = () => {
|
||||
const tags = useTags();
|
||||
const { tag, limit, page } = useParams<RouteParams>();
|
||||
const {
|
||||
size,
|
||||
start,
|
||||
pageInt,
|
||||
pages,
|
||||
results,
|
||||
linkBuild
|
||||
} = useSearch({
|
||||
key: tagKey(tag),
|
||||
fetcher: (start, size) => api.scry<Search>({
|
||||
app: 'sphinx',
|
||||
path: `/lookup/tag/${start}/${size}/${encodeLookup(tag)}`
|
||||
}),
|
||||
enabled: !!tag,
|
||||
limit,
|
||||
page,
|
||||
linkPrefix: `/tags/${tag}`
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate } = useMutation((hash: string) => {
|
||||
return api.poke<Remove>({
|
||||
app: 'sphinx',
|
||||
mark: 'remove',
|
||||
json: hash
|
||||
})
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(tagKey(tag)(start, size))
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn('w-full space-y-6', !tag && 'm-auto')}>
|
||||
{!tag && <h1 className='text-2xl font-semibold'>Tags</h1>}
|
||||
<div className={cn('h-[136px] overflow-y-auto', tag && 'mb-12')}>
|
||||
<TagCloud tags={tags} />
|
||||
</div>
|
||||
<header className='flex items-center'>
|
||||
<h1 className='text-2xl font-semibold leading-none'>{tag}</h1>
|
||||
</header>
|
||||
{tag && results && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
{tag && <Listings listings={results.listings} remove={mutate} />}
|
||||
{results && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
16
ui/src/pages/Tags.tsx
Normal file
16
ui/src/pages/Tags.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { TagCloud } from '../components/TagCloud';
|
||||
import { useTags } from '../state/tags';
|
||||
|
||||
export const Tags = () => {
|
||||
const tags = useTags();
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='flex items-center'>
|
||||
<h1 className='text-2xl font-semibold'>Tags</h1>
|
||||
</header>
|
||||
<TagCloud tags={tags} />
|
||||
</>
|
||||
)
|
||||
}
|
48
ui/src/state/search.ts
Normal file
48
ui/src/state/search.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { Search } from "../types/sphinx";
|
||||
|
||||
interface UseSearchParams {
|
||||
key: (start: number, size: number) => string;
|
||||
fetcher: (start: number, size: number) => Promise<Search>;
|
||||
enabled: boolean;
|
||||
linkPrefix: string;
|
||||
limit?: string;
|
||||
page?: string;
|
||||
}
|
||||
|
||||
export const useSearch = ({ key, fetcher, enabled, limit, page, linkPrefix }: UseSearchParams) => {
|
||||
const size = parseInt(limit || '10', 10);
|
||||
const pageInt = parseInt(page || '1', 10) - 1;
|
||||
const start = pageInt * size;
|
||||
|
||||
const { data } = useQuery<unknown, unknown, Search>(key(start, size), () => fetcher(start, size), {
|
||||
enabled,
|
||||
keepPreviousData: true
|
||||
});
|
||||
|
||||
const total = data?.total || 0;
|
||||
const pages =
|
||||
total % size === 0
|
||||
? total / size
|
||||
: Math.floor(total / size) + 1;
|
||||
|
||||
const linkBuild = useCallback((page) => {
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${linkPrefix}/${size}/${page || 1}`
|
||||
}, [linkPrefix, size]);
|
||||
|
||||
return {
|
||||
results: data || {
|
||||
listings: []
|
||||
},
|
||||
size,
|
||||
pageInt,
|
||||
start,
|
||||
pages,
|
||||
linkBuild
|
||||
}
|
||||
}
|
20
ui/src/state/tags.ts
Normal file
20
ui/src/state/tags.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useMemo } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import api from "../api";
|
||||
import { Tags } from "../types/sphinx";
|
||||
|
||||
export const useTags = () => {
|
||||
const { data } = useQuery('tags', () => api.scry<Tags>({
|
||||
app: 'sphinx',
|
||||
path: '/tags'
|
||||
}));
|
||||
|
||||
return useMemo(() => data ? Object.entries(data).sort(([tagA, a], [tagB, b]) => {
|
||||
if (a === b) {
|
||||
return tagA.localeCompare(tagB);
|
||||
}
|
||||
|
||||
return b - a
|
||||
})
|
||||
: [], [data]);
|
||||
}
|
@ -45,4 +45,8 @@ export interface PostOption {
|
||||
|
||||
export interface PostOptionsForm {
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface Tags {
|
||||
[key: string]: number;
|
||||
}
|
9
ui/src/utils.ts
Normal file
9
ui/src/utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { stringToTa } from "@urbit/api";
|
||||
|
||||
export function encodeLookup(value: string | undefined) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return stringToTa(value).replace('~.', '~~');
|
||||
}
|
Loading…
Reference in New Issue
Block a user