mirror of
https://github.com/arthyn/sphinx.git
synced 2024-12-26 01:12:36 +03:00
mass adding groups/apps
This commit is contained in:
parent
b8d6f728ec
commit
5c2e31a6f1
@ -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'
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
49
ui/src/components/PostOptions.tsx
Normal file
49
ui/src/components/PostOptions.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
58
ui/src/manage-listings/Apps.tsx
Normal file
58
ui/src/manage-listings/Apps.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
65
ui/src/manage-listings/Groups.tsx
Normal file
65
ui/src/manage-listings/Groups.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
5
ui/src/manage-listings/MyListings.tsx
Normal file
5
ui/src/manage-listings/MyListings.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export const MyListings = () => {
|
||||
return <div />;
|
||||
}
|
@ -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
24
ui/src/state/apps.ts
Normal 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
25
ui/src/state/groups.ts
Normal 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']
|
||||
}})) : [];
|
||||
}
|
@ -32,3 +32,12 @@ export type Search = {
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface PostOption {
|
||||
post: Post;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface PostOptionsForm {
|
||||
options: string[];
|
||||
}
|
Loading…
Reference in New Issue
Block a user