adding tag and all browsing

This commit is contained in:
Hunter Miller 2022-07-28 23:09:18 -05:00
parent 5e4bbd8a2e
commit 4f280c97fe
19 changed files with 382 additions and 75 deletions

View File

@ -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

View File

@ -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

View File

@ -9,6 +9,6 @@
--
++ grab
|%
++ noun search
++ noun search:s
--
--

14
desk/mar/tags.hoon Normal file
View 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)
--
--

View File

@ -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 />} />

View File

@ -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>

View 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>
)

View 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>}
</>
)
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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
View 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
View 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
View 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
View 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]);
}

View File

@ -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
View File

@ -0,0 +1,9 @@
import { stringToTa } from "@urbit/api";
export function encodeLookup(value: string | undefined) {
if (!value) {
return '';
}
return stringToTa(value).replace('~.', '~~');
}