mass adding groups/apps

This commit is contained in:
Hunter Miller 2022-07-17 19:23:18 -05:00
parent b8d6f728ec
commit 5c2e31a6f1
13 changed files with 281 additions and 12 deletions

View File

@ -2,7 +2,7 @@
info+'An app for answering all your riddles'
color+0xb2.5068
image+'https://nyc3.digitaloceanspaces.com/hmillerdev/nocsyx-lassul/2022.7.16..04.35.22-sphinx-web-shifted.svg'
glob-http+['http://hmillerdev.nyc3.digitaloceanspaces.com/sphinx/glob-0vb8b8l.rmk5k.kmfli.fbeuf.c82vn.glob' 0vb8b8l.rmk5k.kmfli.fbeuf.c82vn]
glob-http+['https://nyc3.digitaloceanspaces.com/hmillerdev/sphinx/glob-0vb8b8l.rmk5k.kmfli.fbeuf.c82vn.glob' 0vb8b8l.rmk5k.kmfli.fbeuf.c82vn]
base+'sphinx'
version+[0 0 2]
website+'https://github.com'

View File

@ -2,7 +2,10 @@ import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Post } from './pages/Post';
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';
const queryClient = new QueryClient();
@ -12,9 +15,15 @@ function Main() {
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Search />} />
<Route path="/search" element={<Search />} />
<Route path="/search/:lookup" element={<Search />} />
<Route path="/search/:lookup/:limit/:page" element={<Search />} />
<Route path="/post" element={<Post />} />
<Route path="/manage-listings">
<Route index element={<MyListings />} />
<Route path="post" element={<Post />} />
<Route path="apps" element={<Apps />} />
<Route path="groups" element={<Groups />} />
</Route>
</Route>
</Routes>
);

View File

@ -1,6 +1,6 @@
import cn from 'classnames';
import React from 'react';
import { Outlet, useParams } from 'react-router-dom';
import { NavLink, Outlet, useParams } from 'react-router-dom';
import { Meta } from './Meta';
export const Layout = () => {
@ -11,7 +11,28 @@ export const Layout = () => {
<div className="max-w-2xl w-full p-4 sm:py-12 sm:px-8 space-y-6">
<Outlet />
</div>
<Meta className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
<aside className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4'>
<nav className='mb-10'>
<ul className='font-semibold text-sm space-y-4'>
<li>
<NavLink to="/search" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>search</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/post" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>post listing</NavLink>
</li>
<li>
<NavLink to="/manage-listings/apps" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>add apps</NavLink>
</li>
<li>
<NavLink to="/manage-listings/groups" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>add groups</NavLink>
</li>
</ul>
</nav>
<Meta className='text-sm'/>
</aside>
</main>
)
}

View File

@ -36,10 +36,6 @@ export const Meta = ({ className }: MetaProps) => {
>
~nocsyx-lassul/sphinx
</a>
<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">
<PlusIcon className='h-4 w-4 mr-1' />
Add a Listing
</Link>
</footer>
)
}

View File

@ -0,0 +1,49 @@
import { CheckIcon } from '@heroicons/react/solid';
import cn from 'classnames';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { PostOption, PostOptionsForm } from '../types/sphinx';
interface PostOptionsProps {
options: PostOption[];
emptyMessage: string;
className?: string;
}
export const PostOptions = ({ options, emptyMessage, className }: PostOptionsProps) => {
const { register } = useFormContext<PostOptionsForm>();
if (options.length === 0) {
return (
<div>
<h2 className='text-xl font-semibold'>{emptyMessage}</h2>
</div>
)
}
return (
<ul className={cn('space-y-3', className)}>
{options && options.map(o => (
<li key={o.key}>
<input {...register('options')} id={o.key} value={o.key} type="checkbox" className='sr-only' />
<label htmlFor={o.key} className='checked-bg-fawn group-1 flex w-full p-2 hover:bg-fawn cursor-pointer rounded-xl'>
{o.post.image ? (
<img src={o.post.image} className="w-16 h-16 object-cover rounded-lg" />
) : <div className='w-16 h-16 bg-rosy/20 rounded-lg' />}
<div className='flex-1 ml-4'>
<span className='block font-semibold text-lg leading-none mb-1'>{o.post.title}</span>
<div className='font-mono text-xs space-x-3'>
<strong>%{o.post.type}</strong>
<span>{o.post.link.replace('web+urbitgraph://', '')}</span>
</div>
<p className='text-sm'>{o.post.description}</p>
</div>
<div className='flex items-center justify-center m-5 h-6 w-6 border-2 border-solid border-mauve rounded'>
<CheckIcon className='show-when-checked h-4 w-4 opacity-0' />
</div>
</label>
</li>
))}
</ul>
)
}

View File

@ -5,3 +5,12 @@
.flip {
transform: rotateY(180deg);
}
input:checked + label .show-when-checked {
@apply opacity-100;
}
input:checked + .checked-bg-fawn,
input:checked + label .checked-bg-fawn {
@apply bg-fawn;
}

View File

@ -0,0 +1,58 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Link } from 'react-router-dom';
import { PostOptions } from '../components/PostOptions';
import { useApps } from '../state/apps';
import { PostOption, PostOptionsForm } from '../types/sphinx';
function getAppKeys(apps: PostOption[]): string[] {
return apps.map(({ key }) => key);
}
export const Apps = () => {
const apps = useApps();
const form = useForm<PostOptionsForm>({
defaultValues: {
options: []
}
});
const { handleSubmit, reset, watch, setValue } = form;
const options = watch('options');
const toggleAll = useCallback(() => {
if (options.length !== apps.length) {
setValue('options', getAppKeys(apps));
} else {
setValue('options', []);
}
}, [apps, options, setValue]);
const onSubmit = useCallback(() => {
reset();
}, [reset]);
return (
<>
<header className='flex items-center'>
<h1 className='text-2xl font-semibold'>Add Apps</h1>
<button className='p-2 ml-auto text-sm underline font-semibold' onClick={toggleAll}>
{options.length !== apps.length && 'select all'}
{options.length === apps.length && 'deselect all'}
</button>
</header>
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<PostOptions options={apps} emptyMessage="No apps found" />
<div className='flex justify-between border-t border-zinc-300 py-3 mt-6'>
<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'>
Back to Search
</Link>
<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}>
Publish
</button>
</div>
</form>
</FormProvider>
</>
);
}

View File

@ -0,0 +1,65 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Link } from 'react-router-dom';
import { PostOptions } from '../components/PostOptions';
import { useGroups } from '../state/groups';
import { PostOption, PostOptionsForm } from '../types/sphinx';
export const Groups = () => {
const groups = useGroups();
const [firstLoad, setFirstLoad] = useState(true);
const form = useForm<PostOptionsForm>({
defaultValues: {
options: []
}
});
const { handleSubmit, reset, watch, setValue } = form;
const options = watch('options');
useEffect(() => {
if (groups.length > 0 && firstLoad) {
reset({
options: []
})
setFirstLoad(false);
}
}, [groups, firstLoad]);
const toggleAll = useCallback(() => {
if (options.length !== groups.length) {
setValue('options', groups.map(g => g.key));
} else {
setValue('options', []);
}
}, [groups, options, setValue]);
const onSubmit = useCallback(() => {
reset();
}, [reset]);
return (
<>
<header className='flex items-center'>
<h1 className='text-2xl font-semibold'>Add Groups</h1>
<button className='p-2 ml-auto text-sm underline font-semibold' onClick={toggleAll}>
{options.length !== groups.length && 'select all'}
{options.length === groups.length && 'deselect all'}
</button>
</header>
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<PostOptions options={groups} emptyMessage="No groups found" />
<div className='flex justify-between border-t border-zinc-300 py-3 mt-6'>
<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'>
Back to Search
</Link>
<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}>
Publish
</button>
</div>
</form>
</FormProvider>
</>
);
}

View File

@ -0,0 +1,5 @@
import React from 'react';
export const MyListings = () => {
return <div />;
}

View File

@ -17,7 +17,6 @@ function errorMessages(length: number) {
}
export const Post = () => {
const navigate = useNavigate();
const [tags, setTags] = useState<MultiValue<Option>>([]);
const [image, setImage] = useState<string>('');
const form = useForm<PostForm>({
@ -108,9 +107,9 @@ export const Post = () => {
</div>
<div className='pt-3'>
<div className='flex justify-between border-t border-zinc-300 py-3'>
<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)}>
<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'>
Back to Search
</button>
</Link>
<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'>
Publish
</button>

24
ui/src/state/apps.ts Normal file
View File

@ -0,0 +1,24 @@
import { Charge, ChargeUpdateInitial, getVats, scryCharges, Vat, Vats } from "@urbit/api";
import { useQuery } from "react-query"
import api from "../api";
import { Post } from "../types/sphinx";
function getAppPost(app: string, vat: Vat, charge: Charge): Post {
const ship = vat.arak.rail?.publisher || vat.arak.rail?.ship;
return {
type: 'app',
title: charge.title,
link: `${ship}/${app}`,
description: charge.info || charge.title,
image: charge.image || '',
tags: ['app']
}
}
export const useApps = () => {
const { data: vats } = useQuery('vats', () => api.scry<Vats>(getVats));
const { data: charges } = useQuery('charges', () => api.scry<ChargeUpdateInitial>(scryCharges));
return vats && charges ? Object.entries(charges.initial).filter(([k]) => k in vats).map(([k,v]) => ({ key: k, post: getAppPost(k, vats[k], v) })) : [];
}

25
ui/src/state/groups.ts Normal file
View File

@ -0,0 +1,25 @@
import { GroupUpdateInitial, MetadataUpdateInitial } from "@urbit/api";
import { useQuery, useQueryClient } from "react-query"
import api from "../api"
import { PostOption } from "../types/sphinx";
export const useGroups = (): PostOption[] => {
const { data } = useQuery('metadata', () => api.subscribeOnce<{ 'metadata-update': MetadataUpdateInitial }>('metadata-store', '/all'));
const { data: groupData } = useQuery('groups', () => api.subscribeOnce<{ groupUpdate: GroupUpdateInitial }>('group-store', '/groups'));
//web+urbitgraph://group/~nocsyx-lassul/celestial-systems
console.log(data, groupData)
return data && groupData ? Object.entries(data['metadata-update'].associations)
.filter(([k,v]) => v['app-name'] === 'groups' && 'open' in (groupData.groupUpdate.initial[v.resource]?.policy || {}))
.map(([k,v]) => ({ key: k, post: {
type: 'group',
title: v.metadata.title,
link: v.group.replace('/ship/', 'web+urbitgraph://group/'),
image: v.metadata.picture,
description: v.metadata.description,
tags: ['group']
}})) : [];
}

View File

@ -32,3 +32,12 @@ export type Search = {
size: number;
total: number;
}
export interface PostOption {
post: Post;
key: string;
}
export interface PostOptionsForm {
options: string[];
}