on master help sign in
mass adding groups/apps
commit5c2e31a6f12b5d9eb1dbfb800ce7594334cfcd51 authorHunter Miller <hunter@hmiller.dev> dateSun 17 Jul 2022, 19:23 (4 years ago) parentb8d6f72
13 files changed+281−12
desk/desk.docket-0+1−1 ui/src/app.tsx+11−2 ui/src/components/Layout.tsx+23−2 ui/src/components/Meta.tsx−4 ui/src/components/PostOptions.tsx+49 ui/src/index.css+9 ui/src/manage-listings/Apps.tsx+58 ui/src/manage-listings/Groups.tsx+65 ui/src/manage-listings/MyListings.tsx+5 ui/src/pages/Post.tsx → ui/src/manage-listings/Post.tsx+2−3 ui/src/state/apps.ts+24 ui/src/state/groups.ts+25 ui/src/types/sphinx.ts+9
desk/desk.docket-0+1−1
@@ -2,7 +2,7 @@
22 info+'An app for answering all your riddles'
33 color+0xb2.5068
44 image+'https://nyc3.digitaloceanspaces.com/hmillerdev/nocsyx-lassul/2022.7.16..04.35.22-sphinx-web-shifted.svg'
5− glob-http+['http://hmillerdev.nyc3.digitaloceanspaces.com/sphinx/glob-0vb8b8l.rmk5k.kmfli.fbeuf.c82vn.glob' 0vb8b8l.rmk5k.kmfli.fbeuf.c82vn]
5+ glob-http+['https://nyc3.digitaloceanspaces.com/hmillerdev/sphinx/glob-0vb8b8l.rmk5k.kmfli.fbeuf.c82vn.glob' 0vb8b8l.rmk5k.kmfli.fbeuf.c82vn]
66 base+'sphinx'
77 version+[0 0 2]
88 website+'https://github.com'
ui/src/app.tsx+11−2
@@ -2,7 +2,10 @@ import React from 'react';
22import { QueryClient, QueryClientProvider } from 'react-query';
33import { BrowserRouter, Route, Routes } from 'react-router-dom';
44import { Layout } from './components/Layout';
5−import { Post } from './pages/Post';
5+import { Apps } from './manage-listings/Apps';
6+import { Groups } from './manage-listings/Groups';
7+import { MyListings } from './manage-listings/MyListings';
8+import { Post } from './manage-listings/Post';
69import { Search } from './pages/Search';
710
811const queryClient = new QueryClient();
@@ -12,9 +15,15 @@ function Main() {
1215 <Routes>
1316 <Route element={<Layout />}>
1417 <Route path="/" element={<Search />} />
18+ <Route path="/search" element={<Search />} />
1519 <Route path="/search/:lookup" element={<Search />} />
1620 <Route path="/search/:lookup/:limit/:page" element={<Search />} />
17− <Route path="/post" element={<Post />} />
21+ <Route path="/manage-listings">
22+ <Route index element={<MyListings />} />
23+ <Route path="post" element={<Post />} />
24+ <Route path="apps" element={<Apps />} />
25+ <Route path="groups" element={<Groups />} />
26+ </Route>
1827 </Route>
1928 </Routes>
2029 );
ui/src/components/Layout.tsx+23−2
@@ -1,6 +1,6 @@
11import cn from 'classnames';
22import React from 'react';
3−import { Outlet, useParams } from 'react-router-dom';
3+import { NavLink, Outlet, useParams } from 'react-router-dom';
44import { Meta } from './Meta';
55
66export const Layout = () => {
@@ -11,7 +11,28 @@ export const Layout = () => {
1111 <div className="max-w-2xl w-full p-4 sm:py-12 sm:px-8 space-y-6">
1212 <Outlet />
1313 </div>
14− <Meta className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
14+ <aside className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4'>
15+ <nav className='mb-10'>
16+ <ul className='font-semibold text-sm space-y-4'>
17+ <li>
18+ <NavLink to="/search" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>search</NavLink>
19+ </li>
20+ <li>
21+ <NavLink to="/manage-listings" end className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>my listings</NavLink>
22+ </li>
23+ <li>
24+ <NavLink to="/manage-listings/post" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>post listing</NavLink>
25+ </li>
26+ <li>
27+ <NavLink to="/manage-listings/apps" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>add apps</NavLink>
28+ </li>
29+ <li>
30+ <NavLink to="/manage-listings/groups" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>add groups</NavLink>
31+ </li>
32+ </ul>
33+ </nav>
34+ <Meta className='text-sm'/>
35+ </aside>
1536 </main>
1637 )
1738}
ui/src/components/Meta.tsx−4
@@ -36,10 +36,6 @@ export const Meta = ({ className }: MetaProps) => {
3636 >
3737 ~nocsyx-lassul/sphinx
3838 </a>
39− <Link to="/post" className="flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy p-2 transition-colors">
40− <PlusIcon className='h-4 w-4 mr-1' />
41− Add a Listing
42− </Link>
4339 </footer>
4440 )
4541}
ui/src/components/PostOptions.tsxnew file+49
@@ -0,0 +1,49 @@
1+import { CheckIcon } from '@heroicons/react/solid';
2+import cn from 'classnames';
3+import React from 'react';
4+import { useFormContext } from 'react-hook-form';
5+import { PostOption, PostOptionsForm } from '../types/sphinx';
6+
7+interface PostOptionsProps {
8+ options: PostOption[];
9+ emptyMessage: string;
10+ className?: string;
11+}
12+
13+export const PostOptions = ({ options, emptyMessage, className }: PostOptionsProps) => {
14+ const { register } = useFormContext<PostOptionsForm>();
15+
16+ if (options.length === 0) {
17+ return (
18+ <div>
19+ <h2 className='text-xl font-semibold'>{emptyMessage}</h2>
20+ </div>
21+ )
22+ }
23+
24+ return (
25+ <ul className={cn('space-y-3', className)}>
26+ {options && options.map(o => (
27+ <li key={o.key}>
28+ <input {...register('options')} id={o.key} value={o.key} type="checkbox" className='sr-only' />
29+ <label htmlFor={o.key} className='checked-bg-fawn group-1 flex w-full p-2 hover:bg-fawn cursor-pointer rounded-xl'>
30+ {o.post.image ? (
31+ <img src={o.post.image} className="w-16 h-16 object-cover rounded-lg" />
32+ ) : <div className='w-16 h-16 bg-rosy/20 rounded-lg' />}
33+ <div className='flex-1 ml-4'>
34+ <span className='block font-semibold text-lg leading-none mb-1'>{o.post.title}</span>
35+ <div className='font-mono text-xs space-x-3'>
36+ <strong>%{o.post.type}</strong>
37+ <span>{o.post.link.replace('web+urbitgraph://', '')}</span>
38+ </div>
39+ <p className='text-sm'>{o.post.description}</p>
40+ </div>
41+ <div className='flex items-center justify-center m-5 h-6 w-6 border-2 border-solid border-mauve rounded'>
42+ <CheckIcon className='show-when-checked h-4 w-4 opacity-0' />
43+ </div>
44+ </label>
45+ </li>
46+ ))}
47+ </ul>
48+ )
49+}
ui/src/index.css+9
@@ -4,4 +4,13 @@
44
55.flip {
66 transform: rotateY(180deg);
7+}
8+
9+input:checked + label .show-when-checked {
10+ @apply opacity-100;
11+}
12+
13+input:checked + .checked-bg-fawn,
14+input:checked + label .checked-bg-fawn {
15+ @apply bg-fawn;
716}
ui/src/manage-listings/Apps.tsxnew file+58
@@ -0,0 +1,58 @@
1+import React, { useCallback, useEffect, useState } from 'react';
2+import { FormProvider, useForm } from 'react-hook-form';
3+import { Link } from 'react-router-dom';
4+import { PostOptions } from '../components/PostOptions';
5+import { useApps } from '../state/apps';
6+import { PostOption, PostOptionsForm } from '../types/sphinx';
7+
8+function getAppKeys(apps: PostOption[]): string[] {
9+ return apps.map(({ key }) => key);
10+}
11+
12+export const Apps = () => {
13+ const apps = useApps();
14+ const form = useForm<PostOptionsForm>({
15+ defaultValues: {
16+ options: []
17+ }
18+ });
19+ const { handleSubmit, reset, watch, setValue } = form;
20+ const options = watch('options');
21+
22+ const toggleAll = useCallback(() => {
23+ if (options.length !== apps.length) {
24+ setValue('options', getAppKeys(apps));
25+ } else {
26+ setValue('options', []);
27+ }
28+ }, [apps, options, setValue]);
29+
30+ const onSubmit = useCallback(() => {
31+ reset();
32+ }, [reset]);
33+
34+ return (
35+ <>
36+ <header className='flex items-center'>
37+ <h1 className='text-2xl font-semibold'>Add Apps</h1>
38+ <button className='p-2 ml-auto text-sm underline font-semibold' onClick={toggleAll}>
39+ {options.length !== apps.length && 'select all'}
40+ {options.length === apps.length && 'deselect all'}
41+ </button>
42+ </header>
43+ <FormProvider {...form}>
44+ <form onSubmit={handleSubmit(onSubmit)}>
45+ <PostOptions options={apps} emptyMessage="No apps found" />
46+ <div className='flex justify-between border-t border-zinc-300 py-3 mt-6'>
47+ <Link to="/search" className='flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy leading-none py-2 px-3 transition-colors'>
48+ Back to Search
49+ </Link>
50+ <button type="submit" className='flex items-center rounded-lg text-base font-semibold text-linen bg-rosy disabled:bg-zinc-200 disabled:text-zinc-400 disabled:border-transparent border-2 border-transparent hover:border-linen/60 leading-none py-2 px-3 transition-colors' disabled={options.length === 0}>
51+ Publish
52+ </button>
53+ </div>
54+ </form>
55+ </FormProvider>
56+ </>
57+ );
58+}
ui/src/manage-listings/Groups.tsxnew file+65
@@ -0,0 +1,65 @@
1+import React, { useCallback, useEffect, useState } from 'react';
2+import { FormProvider, useForm } from 'react-hook-form';
3+import { Link } from 'react-router-dom';
4+import { PostOptions } from '../components/PostOptions';
5+import { useGroups } from '../state/groups';
6+import { PostOption, PostOptionsForm } from '../types/sphinx';
7+
8+export const Groups = () => {
9+ const groups = useGroups();
10+ const [firstLoad, setFirstLoad] = useState(true);
11+ const form = useForm<PostOptionsForm>({
12+ defaultValues: {
13+ options: []
14+ }
15+ });
16+ const { handleSubmit, reset, watch, setValue } = form;
17+ const options = watch('options');
18+
19+ useEffect(() => {
20+ if (groups.length > 0 && firstLoad) {
21+ reset({
22+ options: []
23+ })
24+
25+ setFirstLoad(false);
26+ }
27+ }, [groups, firstLoad]);
28+
29+ const toggleAll = useCallback(() => {
30+ if (options.length !== groups.length) {
31+ setValue('options', groups.map(g => g.key));
32+ } else {
33+ setValue('options', []);
34+ }
35+ }, [groups, options, setValue]);
36+
37+ const onSubmit = useCallback(() => {
38+ reset();
39+ }, [reset]);
40+
41+ return (
42+ <>
43+ <header className='flex items-center'>
44+ <h1 className='text-2xl font-semibold'>Add Groups</h1>
45+ <button className='p-2 ml-auto text-sm underline font-semibold' onClick={toggleAll}>
46+ {options.length !== groups.length && 'select all'}
47+ {options.length === groups.length && 'deselect all'}
48+ </button>
49+ </header>
50+ <FormProvider {...form}>
51+ <form onSubmit={handleSubmit(onSubmit)}>
52+ <PostOptions options={groups} emptyMessage="No groups found" />
53+ <div className='flex justify-between border-t border-zinc-300 py-3 mt-6'>
54+ <Link to="/search" className='flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy leading-none py-2 px-3 transition-colors'>
55+ Back to Search
56+ </Link>
57+ <button type="submit" className='flex items-center rounded-lg text-base font-semibold text-linen bg-rosy disabled:bg-zinc-200 disabled:text-zinc-400 disabled:border-transparent border-2 border-transparent hover:border-linen/60 leading-none py-2 px-3 transition-colors' disabled={options.length === 0}>
58+ Publish
59+ </button>
60+ </div>
61+ </form>
62+ </FormProvider>
63+ </>
64+ );
65+}
ui/src/manage-listings/MyListings.tsxnew file+5
@@ -0,0 +1,5 @@
1+import React from 'react';
2+
3+export const MyListings = () => {
4+ return <div />;
5+}
ui/src/pages/Post.tsx → ui/src/manage-listings/Post.tsxrenamed+2−3
@@ -17,7 +17,6 @@ function errorMessages(length: number) {
1717}
1818
1919export const Post = () => {
20− const navigate = useNavigate();
2120 const [tags, setTags] = useState<MultiValue<Option>>([]);
2221 const [image, setImage] = useState<string>('');
2322 const form = useForm<PostForm>({
@@ -108,9 +107,9 @@ export const Post = () => {
108107 </div>
109108 <div className='pt-3'>
110109 <div className='flex justify-between border-t border-zinc-300 py-3'>
111− <button type="button" className='flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy leading-none py-2 px-3 transition-colors' onClick={() => navigate(-1)}>
110+ <Link to="/search" className='flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy leading-none py-2 px-3 transition-colors'>
112111 Back to Search
113− </button>
112+ </Link>
114113 <button type="submit" className='flex items-center rounded-lg text-base font-semibold text-linen bg-rosy border-2 border-transparent hover:border-linen/60 leading-none py-2 px-3 transition-colors'>
115114 Publish
116115 </button>
ui/src/state/apps.tsnew file+24
@@ -0,0 +1,24 @@
1+import { Charge, ChargeUpdateInitial, getVats, scryCharges, Vat, Vats } from "@urbit/api";
2+import { useQuery } from "react-query"
3+import api from "../api";
4+import { Post } from "../types/sphinx";
5+
6+function getAppPost(app: string, vat: Vat, charge: Charge): Post {
7+ const ship = vat.arak.rail?.publisher || vat.arak.rail?.ship;
8+
9+ return {
10+ type: 'app',
11+ title: charge.title,
12+ link: `${ship}/${app}`,
13+ description: charge.info || charge.title,
14+ image: charge.image || '',
15+ tags: ['app']
16+ }
17+}
18+
19+export const useApps = () => {
20+ const { data: vats } = useQuery('vats', () => api.scry<Vats>(getVats));
21+ const { data: charges } = useQuery('charges', () => api.scry<ChargeUpdateInitial>(scryCharges));
22+
23+ return vats && charges ? Object.entries(charges.initial).filter(([k]) => k in vats).map(([k,v]) => ({ key: k, post: getAppPost(k, vats[k], v) })) : [];
24+}
ui/src/state/groups.tsnew file+25
@@ -0,0 +1,25 @@
1+import { GroupUpdateInitial, MetadataUpdateInitial } from "@urbit/api";
2+import { useQuery, useQueryClient } from "react-query"
3+import api from "../api"
4+import { PostOption } from "../types/sphinx";
5+
6+
7+
8+export const useGroups = (): PostOption[] => {
9+ const { data } = useQuery('metadata', () => api.subscribeOnce<{ 'metadata-update': MetadataUpdateInitial }>('metadata-store', '/all'));
10+ const { data: groupData } = useQuery('groups', () => api.subscribeOnce<{ groupUpdate: GroupUpdateInitial }>('group-store', '/groups'));
11+
12+ //web+urbitgraph://group/~nocsyx-lassul/celestial-systems
13+ console.log(data, groupData)
14+
15+ return data && groupData ? Object.entries(data['metadata-update'].associations)
16+ .filter(([k,v]) => v['app-name'] === 'groups' && 'open' in (groupData.groupUpdate.initial[v.resource]?.policy || {}))
17+ .map(([k,v]) => ({ key: k, post: {
18+ type: 'group',
19+ title: v.metadata.title,
20+ link: v.group.replace('/ship/', 'web+urbitgraph://group/'),
21+ image: v.metadata.picture,
22+ description: v.metadata.description,
23+ tags: ['group']
24+ }})) : [];
25+}
ui/src/types/sphinx.ts+9
@@ -31,4 +31,13 @@ export type Search = {
3131 limit: number;
3232 size: number;
3333 total: number;
34+}
35+
36+export interface PostOption {
37+ post: Post;
38+ key: string;
39+}
40+
41+export interface PostOptionsForm {
42+ options: string[];
3443}