on master help sign in
better mobile spacing, pals install and unfriend, index on new pal
ad4107aHunter Miller
desk/app/sphinx.hoon+69−62
@@ -152,7 +152,11 @@
152 ::
152 ::
153 %directory
153 %directory
154 =+ !<(d=directory:s vase)
154 =+ !<(d=directory:s vase)
155− cor(directory (~(uni by directory) d))
155+ =. directory (~(uni by directory) d)
156+ %+ roll
157+ ~(tap by d)
158+ |= [[=hash:s l=listing:s] c=_cor]
159+ (index:c hash l)
156 ==
160 ==
157 ==
161 ==
158 ==
162 ==
@@ -229,68 +233,71 @@
229 ++ di-publish
233 ++ di-publish
230 |= l=listing:s
234 |= l=listing:s
231 =. listing l
235 =. listing l
232− ?> (lte (lent (trip title.post.l)) 77)
236+ =. cor (index hash l)
233− ?> (lte (lent (trip description.post.l)) 256)
234− ?> (lte (lent tags.post.l) 8)
235− ?> (lte (lent (trip link.post.l)) 1.024)
236− ?> (lte (lent (trip image.post.l)) 1.024)
237− =. cor
238− ?: =(image.post.l '') cor
239− =/ image (validate image.post.l)
240− cor
241− =/ link (validate link.post.l)
242− :: TODO check urls and sanitize
243 =. published (~(put by directory) hash.listing listing)
237 =. published (~(put by directory) hash.listing listing)
244− =. cor (emit (invent:gossip %directory-listing !>(listing)))
238+ =. cor (emit (invent:gossip %directory-listing !>(listing)))
245− =/ entries=lookup:s
246− %- malt
247− %- zing
248− :~ ~[[(crip (cass (trip title.post.l))) ~[[hash rank=0]]]]
249− %+ turn
250− (sift:sift title.post.l)
251− |= word=@t
252− [word ~[[hash rank=1]]]
253− %+ turn
254− tags.post.l
255− |= tag=@t
256− [(norm:sift tag) ~[[hash rank=2]]]
257− %+ turn
258− (sift:sift description.post.l)
259− |= word=@t
260− [word ~[[hash rank=3]]]
261− ~[[type.post.l ~[[hash rank=4]]]]
262− ==
263− =/ keys ~(tap in ~(key by entries))
264− =. trail (~(put by trail) hash keys)
265− =. lookup
266− %- ~(uni by entries)
267− %- ~(rut by lookup)
268− |= [=key:s value=(list entry:s)]
269− ?. (~(has by entries) key) value
270− (weld value (~(got by entries) key))
271− =. trigrams
272− %+ roll
273− %+ snoc
274− %+ turn keys
275− |= =key:s
276− %- malt
277− %+ turn (iching key)
278− |= =trigram:s
279− [trigram (silt ~[key])]
280− trigrams
281− |= [next=trigrams:s t=trigrams:s]
282− %- (~(uno by t) next)
283− |= [k=key:s v=(set key:s) w=(set key:s)]
284− (~(uni in v) w)
285− =. phonetics
286− %-
287− %- ~(uno by phonetics)
288− %- malt
289− %+ turn keys
290− |= =key:s
291− [(utter:m key) (silt ~[key])]
292− |= [k=key:s v=(set key:s) w=(set key:s)]
293− (~(uni in v) w)
294 di-core
239 di-core
295 --
240 --
241+++ index
242+ |= [=hash:s l=listing:s]
243+ ?> (lte (lent (trip title.post.l)) 77)
244+ ?> (lte (lent (trip description.post.l)) 256)
245+ ?> (lte (lent tags.post.l) 8)
246+ ?> (lte (lent (trip link.post.l)) 1.024)
247+ ?> (lte (lent (trip image.post.l)) 1.024)
248+ =. cor
249+ ?: =(image.post.l '') cor
250+ =/ image (validate image.post.l)
251+ cor
252+ =/ link (validate link.post.l)
253+ =/ entries=lookup:s
254+ %- malt
255+ %- zing
256+ :~ ~[[(crip (cass (trip title.post.l))) ~[[hash rank=0]]]]
257+ %+ turn
258+ (sift:sift title.post.l)
259+ |= word=@t
260+ [word ~[[hash rank=1]]]
261+ %+ turn
262+ tags.post.l
263+ |= tag=@t
264+ [(norm:sift tag) ~[[hash rank=2]]]
265+ %+ turn
266+ (sift:sift description.post.l)
267+ |= word=@t
268+ [word ~[[hash rank=3]]]
269+ ~[[type.post.l ~[[hash rank=4]]]]
270+ ==
271+ =/ keys ~(tap in ~(key by entries))
272+ =. trail (~(put by trail) hash keys)
273+ =. lookup
274+ %- ~(uni by entries)
275+ %- ~(rut by lookup)
276+ |= [=key:s value=(list entry:s)]
277+ ?. (~(has by entries) key) value
278+ (weld value (~(got by entries) key))
279+ =. trigrams
280+ %+ roll
281+ %+ snoc
282+ %+ turn keys
283+ |= =key:s
284+ %- malt
285+ %+ turn (iching key)
286+ |= =trigram:s
287+ [trigram (silt ~[key])]
288+ trigrams
289+ |= [next=trigrams:s t=trigrams:s]
290+ %- (~(uno by t) next)
291+ |= [k=key:s v=(set key:s) w=(set key:s)]
292+ (~(uni in v) w)
293+ =. phonetics
294+ %-
295+ %- ~(uno by phonetics)
296+ %- malt
297+ %+ turn keys
298+ |= =key:s
299+ [(utter:m key) (silt ~[key])]
300+ |= [k=key:s v=(set key:s) w=(set key:s)]
301+ (~(uni in v) w)
302+ cor
296--
303--
desk/lib/sphinx.hoon−1
@@ -105,7 +105,6 @@
105 |= term=@t
105 |= term=@t
106 ^- (list @t)
106 ^- (list @t)
107 =/ tally (get-term-counts term)
107 =/ tally (get-term-counts term)
108− ~& tally
109 =/ tallys ~(tap by tally)
108 =/ tallys ~(tap by tally)
110 =/ count (lent (iching term))
109 =/ count (lent (iching term))
111 %+ turn
110 %+ turn
desk/lib/validate-url.hoon−1
@@ -2,7 +2,6 @@
2++ validate
2++ validate
3 |= url=@t
3 |= url=@t
4 ^- @t
4 ^- @t
5− ~& url
6 %+ rash url
5 %+ rash url
7 %+ cold url
6 %+ cold url
8 ;~ plug
7 ;~ plug
desk/mar/pals/command.hoonnew file+22
@@ -0,0 +1,22 @@
1+:: pals-command: friend management
2+::
3+/- *pals
4+::
5+|_ cmd=command
6+++ grad %noun
7+++ grow
8+ |%
9+ ++ noun cmd
10+ --
11+++ grab
12+ |%
13+ ++ noun command
14+ ++ json
15+ ^- $-(^json command)
16+ =, dejs:format
17+ |^ (of meet+arg part+arg ~)
18+ ++ arg
19+ (ot 'ship'^(su fed:ag) 'in'^(as so) ~)
20+ --
21+ --
22+--
ui/package-lock.json+64−1
@@ -22,7 +22,8 @@
22 "react-hook-form": "^7.33.1",
22 "react-hook-form": "^7.33.1",
23 "react-query": "^3.39.1",
23 "react-query": "^3.39.1",
24 "react-router-dom": "^6.3.0",
24 "react-router-dom": "^6.3.0",
25− "react-select": "^5.4.0"
25+ "react-select": "^5.4.0",
26+ "zustand": "^4.0.0-rc.1"
26 },
27 },
27 "devDependencies": {
28 "devDependencies": {
28 "@types/lodash.debounce": "^4.0.7",
29 "@types/lodash.debounce": "^4.0.7",
@@ -33,6 +34,7 @@
33 "autoprefixer": "^10.4.7",
34 "autoprefixer": "^10.4.7",
34 "postcss": "^8.4.14",
35 "postcss": "^8.4.14",
35 "tailwindcss": "^3.1.4",
36 "tailwindcss": "^3.1.4",
37+ "tailwindcss-labeled-groups": "^0.0.2",
36 "typescript": "^4.3.2",
38 "typescript": "^4.3.2",
37 "vite": "^2.9.13"
39 "vite": "^2.9.13"
38 }
40 }
@@ -3190,6 +3192,15 @@
3190 "postcss": "^8.0.9"
3192 "postcss": "^8.0.9"
3191 }
3193 }
3192 },
3194 },
3195+ "node_modules/tailwindcss-labeled-groups": {
3196+ "version": "0.0.2",
3197+ "resolved": "https://registry.npmjs.org/tailwindcss-labeled-groups/-/tailwindcss-labeled-groups-0.0.2.tgz",
3198+ "integrity": "sha512-leOVv9h2K9WDHoXPXVBOmTJgWECVkpVFc/hjrwuqGUEtNxvRvuOtCjenu9ppY+pJtky5INJUqDCNa8pS3AbYmw==",
3199+ "dev": true,
3200+ "peerDependencies": {
3201+ "tailwindcss": "^3.0.23"
3202+ }
3203+ },
3193 "node_modules/tailwindcss/node_modules/color-name": {
3204 "node_modules/tailwindcss/node_modules/color-name": {
3194 "version": "1.1.4",
3205 "version": "1.1.4",
3195 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
3206 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
@@ -3329,6 +3340,14 @@
3329 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
3340 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
3330 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
3341 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
3331 },
3342 },
3343+ "node_modules/use-sync-external-store": {
3344+ "version": "1.1.0",
3345+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
3346+ "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
3347+ "peerDependencies": {
3348+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
3349+ }
3350+ },
3332 "node_modules/util-deprecate": {
3351 "node_modules/util-deprecate": {
3333 "version": "1.0.2",
3352 "version": "1.0.2",
3334 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
3353 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3420,6 +3439,29 @@
3420 "engines": {
3439 "engines": {
3421 "node": ">= 6"
3440 "node": ">= 6"
3422 }
3441 }
3442+ },
3443+ "node_modules/zustand": {
3444+ "version": "4.0.0-rc.1",
3445+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.0.0-rc.1.tgz",
3446+ "integrity": "sha512-qgcs7zLqBdHu0PuT3GW4WCIY5SgXdsv30GQMu9Qpp1BA2aS+sNS8l4x0hWuyEhjXkN+701aGWawhKDv6oWJAcw==",
3447+ "dependencies": {
3448+ "use-sync-external-store": "1.1.0"
3449+ },
3450+ "engines": {
3451+ "node": ">=12.7.0"
3452+ },
3453+ "peerDependencies": {
3454+ "immer": ">=9.0",
3455+ "react": ">=16.8"
3456+ },
3457+ "peerDependenciesMeta": {
3458+ "immer": {
3459+ "optional": true
3460+ },
3461+ "react": {
3462+ "optional": true
3463+ }
3464+ }
3423 }
3465 }
3424 },
3466 },
3425 "dependencies": {
3467 "dependencies": {
@@ -5661,6 +5703,13 @@
5661 }
5703 }
5662 }
5704 }
5663 },
5705 },
5706+ "tailwindcss-labeled-groups": {
5707+ "version": "0.0.2",
5708+ "resolved": "https://registry.npmjs.org/tailwindcss-labeled-groups/-/tailwindcss-labeled-groups-0.0.2.tgz",
5709+ "integrity": "sha512-leOVv9h2K9WDHoXPXVBOmTJgWECVkpVFc/hjrwuqGUEtNxvRvuOtCjenu9ppY+pJtky5INJUqDCNa8pS3AbYmw==",
5710+ "dev": true,
5711+ "requires": {}
5712+ },
5664 "to-fast-properties": {
5713 "to-fast-properties": {
5665 "version": "2.0.0",
5714 "version": "2.0.0",
5666 "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
5715 "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -5745,6 +5794,12 @@
5745 }
5794 }
5746 }
5795 }
5747 },
5796 },
5797+ "use-sync-external-store": {
5798+ "version": "1.1.0",
5799+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
5800+ "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
5801+ "requires": {}
5802+ },
5748 "util-deprecate": {
5803 "util-deprecate": {
5749 "version": "1.0.2",
5804 "version": "1.0.2",
5750 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
5805 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -5795,6 +5850,14 @@
5795 "version": "1.10.2",
5850 "version": "1.10.2",
5796 "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
5851 "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
5797 "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
5852 "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
5853+ },
5854+ "zustand": {
5855+ "version": "4.0.0-rc.1",
5856+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.0.0-rc.1.tgz",
5857+ "integrity": "sha512-qgcs7zLqBdHu0PuT3GW4WCIY5SgXdsv30GQMu9Qpp1BA2aS+sNS8l4x0hWuyEhjXkN+701aGWawhKDv6oWJAcw==",
5858+ "requires": {
5859+ "use-sync-external-store": "1.1.0"
5860+ }
5798 }
5861 }
5799 }
5862 }
5800}
5863}
ui/package.json+3−1
@@ -24,7 +24,8 @@
24 "react-hook-form": "^7.33.1",
24 "react-hook-form": "^7.33.1",
25 "react-query": "^3.39.1",
25 "react-query": "^3.39.1",
26 "react-router-dom": "^6.3.0",
26 "react-router-dom": "^6.3.0",
27− "react-select": "^5.4.0"
27+ "react-select": "^5.4.0",
28+ "zustand": "^4.0.0-rc.1"
28 },
29 },
29 "devDependencies": {
30 "devDependencies": {
30 "@types/lodash.debounce": "^4.0.7",
31 "@types/lodash.debounce": "^4.0.7",
@@ -35,6 +36,7 @@
35 "autoprefixer": "^10.4.7",
36 "autoprefixer": "^10.4.7",
36 "postcss": "^8.4.14",
37 "postcss": "^8.4.14",
37 "tailwindcss": "^3.1.4",
38 "tailwindcss": "^3.1.4",
39+ "tailwindcss-labeled-groups": "^0.0.2",
38 "typescript": "^4.3.2",
40 "typescript": "^4.3.2",
39 "vite": "^2.9.13"
41 "vite": "^2.9.13"
40 }
42 }
ui/src/components/Filter.tsx+1−1
@@ -34,7 +34,7 @@ export function Filter<T extends string>({ selected, onSelect, className, showAl
34 <div className={className}>
34 <div className={className}>
35 <Select.Root value={selected} onValueChange={onSelect}>
35 <Select.Root value={selected} onValueChange={onSelect}>
36 <span className="sr-only">Filter Type</span>
36 <span className="sr-only">Filter Type</span>
37− <Select.Trigger className="bg-linen flex justify-between w-32 font-semibold border border-fawn/30 rounded-lg shadow-sm pl-3 pr-2 py-1.5 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-rosy focus:border-rosy sm:text-sm">
37+ <Select.Trigger className="bg-linen flex justify-between w-full font-semibold border border-fawn/30 rounded-lg shadow-sm pl-3 pr-2 py-1.5 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-rosy focus:border-rosy sm:text-sm">
38 <Select.Value>%{selected}</Select.Value>
38 <Select.Value>%{selected}</Select.Value>
39 <Select.Icon asChild>
39 <Select.Icon asChild>
40 <svg className="h-5 w-5 text-rosy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
40 <svg className="h-5 w-5 text-rosy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
ui/src/components/Layout.tsx+2−2
@@ -8,10 +8,10 @@ export const Layout = () => {
8
8
9 return (
9 return (
10 <main className={cn("flex flex-col items-center min-h-screen", !params.lookup && 'justify-center')}>
10 <main className={cn("flex flex-col items-center min-h-screen", !params.lookup && 'justify-center')}>
11− <div className="max-w-2xl w-full p-4 sm:py-12 px-8 space-y-6">
11+ <div className="max-w-2xl w-full p-4 sm:py-12 sm:px-8 space-y-6">
12 <Outlet />
12 <Outlet />
13 </div>
13 </div>
14− <Meta className='self-start mx-10 my-6 sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
14+ <Meta className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
15 </main>
15 </main>
16 )
16 )
17}
17}
ui/src/components/Listings.tsxnew file+70
@@ -0,0 +1,70 @@
1+import { UserRemoveIcon, UsersIcon } from '@heroicons/react/solid';
2+import { deSig } from '@urbit/api';
3+import cn from 'classnames';
4+import React from 'react';
5+import { usePals } from '../state/pals';
6+import { Listing } from '../types/sphinx';
7+
8+interface ListingsProps {
9+ listings: Listing[];
10+ remove: (hash: string) => void;
11+ className?: string;
12+}
13+
14+export const Listings = ({ listings, remove, className }: ListingsProps) => {
15+ const { outgoing, remove: removePal } = usePals();
16+ console.log(outgoing);
17+ if (!listings) {
18+ return null;
19+ }
20+
21+ if (listings.length === 0) {
22+ return (
23+ <div>
24+ <h2 className='text-xl font-semibold'>No Results</h2>
25+ </div>
26+ )
27+ }
28+
29+ return (
30+ <ul className={cn('px-2 space-y-6', className)}>
31+ {listings && listings.map(l => (
32+ <li key={l.hash}>
33+ <article className='group-1 flex w-full'>
34+ {l.post.image ? (
35+ <img src={l.post.image} className="w-24 h-24 object-cover rounded-xl" />
36+ ) : <div className='w-24 h-24 bg-rosy/20 rounded-xl' />}
37+ <div className='flex-1 ml-4'>
38+ <a className='block w-full mb-2 hover:text-rosy transition-colors' href={l.post.link}>
39+ <h2 className='block font-semibold text-lg leading-none mb-1'>{l.post.title}</h2>
40+ <div className='font-mono text-xs space-x-3'>
41+ <strong>%{l.post.type}</strong>
42+ <span>{l.post.link.replace('web+urbitgraph://', '')}</span>
43+ </div>
44+ </a>
45+ <p className='mb-2 text-sm'>{l.post.description}</p>
46+ <div className='flex flex-col items-start sm:flex-row sm:items-center w-full font-mono text-xs text-zinc-500 opacity-0 group-1-hover:opacity-100 transition-opacity'>
47+ <div className='flex items-center'>
48+ <span className='inline-flex items-center'>
49+ <span className='font-semibold font-sans mr-1'>by:</span>
50+ <span>{l.source}</span>
51+ {outgoing[deSig(l.source) || ''] && (
52+ <button className='group-2 relative inline-flex items-center justify-center px-2 hover:text-rosy' aria-label='Remove pal' title="Remove Pal" onClick={() => removePal(deSig(l.source))}>
53+ <UsersIcon className='h-4 w-4 group-2-hover:opacity-0' />
54+ <UserRemoveIcon className='absolute h-4 w-4 opacity-0 group-2-hover:opacity-100' />
55+ </button>
56+ )}
57+ </span>
58+ <time dateTime={(new Date(l.time)).toISOString()} className="ml-3">
59+ {(new Date(l.time)).toLocaleString()}
60+ </time>
61+ </div>
62+ <button className='sm:ml-auto font-semibold font-sans hover:text-rosy bg-transparent border-0' onClick={() => remove(l.hash)}>remove</button>
63+ </div>
64+ </div>
65+ </article>
66+ </li>
67+ ))}
68+ </ul>
69+ )
70+}
ui/src/components/SearchList.tsxdeleted−56
@@ -1,56 +0,0 @@
1−import cn from 'classnames';
2−import React, { useCallback } from 'react';
3−import { useMutation, useQueryClient } from 'react-query';
4−import api from '../api';
5−import { Listing, Remove } from '../types/sphinx';
6−
7−interface SearchListProps {
8− listings: Listing[];
9− remove: (hash: string) => void;
10− className?: string;
11−}
12−
13−export const SearchList = ({ listings, remove, className }: SearchListProps) => {
14− if (!listings) {
15− return null;
16− }
17−
18− if (listings.length === 0) {
19− return (
20− <div>
21− <h2 className='text-xl font-semibold'>No Results</h2>
22− </div>
23− )
24− }
25−
26− return (
27− <ul className={cn('px-2 space-y-6', className)}>
28− {listings && listings.map(l => (
29− <li key={l.hash}>
30− <article className='group flex w-full'>
31− {l.post.image ? (
32− <img src={l.post.image} className="w-24 h-24 object-cover rounded-xl" />
33− ) : <div className='w-24 h-24 bg-rosy/20 rounded-xl' />}
34− <div className='flex-1 ml-4'>
35− <a className='block w-full mb-2 hover:text-rosy transition-colors' href={l.post.link}>
36− <h2 className='block font-semibold text-lg leading-none mb-1'>{l.post.title}</h2>
37− <div className='font-mono text-xs space-x-3'>
38− <strong>%{l.post.type}</strong>
39− <span>{l.post.link.replace('web+urbitgraph://', '')}</span>
40− </div>
41− </a>
42− <p className='mb-2 text-sm'>{l.post.description}</p>
43− <div className='flex items-center w-full font-mono text-xs text-zinc-500 opacity-0 group-hover:opacity-100 transition-opacity'>
44− <span><span className='font-semibold font-sans'>by:</span> {l.source}</span>
45− <time dateTime={(new Date(l.time)).toISOString()} className="ml-3">
46− {(new Date(l.time)).toLocaleString()}
47− </time>
48− <button className='ml-auto font-semibold font-sans hover:text-rosy bg-transparent border-0' onClick={() => remove(l.hash)}>remove</button>
49− </div>
50− </div>
51− </article>
52− </li>
53− ))}
54− </ul>
55− )
56−}
ui/src/pages/Search.tsx+21−11
@@ -1,17 +1,17 @@
1import cn from 'classnames';
1import cn from 'classnames';
2−import React, { useCallback, useRef, useState } from 'react';
2+import React, { useCallback, useEffect, useRef, useState } from 'react';
3import debounce from 'lodash.debounce';
3import debounce from 'lodash.debounce';
4−import { SearchInput } from '../components/SearchInput';
5−import { SearchList } from '../components/SearchList';
6import { useMutation, useQuery, useQueryClient } from 'react-query';
4import { useMutation, useQuery, useQueryClient } from 'react-query';
5+import { useNavigate, useParams } from 'react-router-dom';
6+import { stringToTa } from '@urbit/api';
7+import { SearchInput } from '../components/SearchInput';
8+import { Listings } from '../components/Listings';
7−import { PostFilter, PostType, Remove, Search as SearchType } from '../types/sphinx';
9+import { PostFilter, Remove, Search as SearchType } from '../types/sphinx';
8import api from '../api';
10import api from '../api';
9−import { Link, useNavigate, useParams } from 'react-router-dom';
10import { Paginator } from '../components/Paginator';
11import { Paginator } from '../components/Paginator';
11−import { stringToTa } from '@urbit/api';
12import { Filter } from '../components/Filter';
12import { Filter } from '../components/Filter';
13−import { Meta } from '../components/Meta';
14−import { DocumentAddIcon, PlusCircleIcon, PlusIcon } from '@heroicons/react/solid';
13+import { PlusSmIcon } from '@heroicons/react/solid';
14+import { usePals } from '../state/pals';
15
15
16interface RouteParams extends Record<string, string | undefined> {
16interface RouteParams extends Record<string, string | undefined> {
17 lookup?: string;
17 lookup?: string;
@@ -36,6 +36,7 @@ export const Search = () => {
36 } = useParams<RouteParams>();
36 } = useParams<RouteParams>();
37 const [selected, setSelected] = useState<PostFilter>('all')
37 const [selected, setSelected] = useState<PostFilter>('all')
38 const [rawSearch, setRawSearch] = useState(lookup || '');
38 const [rawSearch, setRawSearch] = useState(lookup || '');
39+ const { installed: palsInstalled } = usePals();
39 const size = parseInt(limit || '10', 10);
40 const size = parseInt(limit || '10', 10);
40 const pageInt = parseInt(page || '1', 10) - 1;
41 const pageInt = parseInt(page || '1', 10) - 1;
41 const start = pageInt * size;
42 const start = pageInt * size;
@@ -57,7 +58,7 @@ export const Search = () => {
57 onSuccess: () => {
58 onSuccess: () => {
58 queryClient.invalidateQueries(`lookup-${selected}-${lookup}-${size}-${start}`)
59 queryClient.invalidateQueries(`lookup-${selected}-${lookup}-${size}-${start}`)
59 }
60 }
60− })
61+ });
61
62
62 const total = data?.total || 0;
63 const total = data?.total || 0;
63 const pages =
64 const pages =
@@ -90,12 +91,21 @@ export const Search = () => {
90 <>
91 <>
91 <header className='flex items-center space-x-2'>
92 <header className='flex items-center space-x-2'>
92 <SearchInput className='flex-1' lookup={rawSearch} onChange={onChange} />
93 <SearchInput className='flex-1' lookup={rawSearch} onChange={onChange} />
93− <Filter selected={selected} onSelect={setSelected} />
94+ <Filter selected={selected} onSelect={setSelected} className="min-w-0 sm:w-20" />
94 </header>
95 </header>
96+ {!palsInstalled && (
97+ <div>
98+ <a href="/apps/grid/leap/search/~paldev/apps/~paldev/pals" className='inline-flex items-center py-1 px-2 mr-2 font-semibold bg-green-800 text-linen rounded-md'>
99+ <PlusSmIcon className='h-4 w-4' />
100+ install pals
101+ </a>
102+ to see listings from others
103+ </div>
104+ )}
95 {lookup && data && <div className='flex justify-end border-t border-zinc-300'>
105 {lookup && data && <div className='flex justify-end border-t border-zinc-300'>
96 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
106 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
97 </div>}
107 </div>}
98− {lookup && <SearchList listings={data?.listings || []} remove={mutate} />}
108+ {lookup && <Listings listings={data?.listings || []} remove={mutate} />}
99 {data && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
109 {data && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
100 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
110 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
101 </div>}
111 </div>}
ui/src/state/pals.tsnew file+61
@@ -0,0 +1,61 @@
1+import { ChargeUpdateInitial, scryCharges } from '@urbit/api';
2+import { useMutation, useQuery, useQueryClient } from 'react-query';
3+import api from '../api';
4+
5+interface Pal {
6+ lists: string[];
7+ ack: true | null;
8+}
9+
10+interface Pals {
11+ [k: string]: Pal;
12+}
13+
14+interface PalsData {
15+ incoming: Pals;
16+ outgoing: Pals;
17+ [k: string]: Pal | Pals;
18+}
19+
20+interface RemovePal {
21+ part: {
22+ ship: string;
23+ in: string[];
24+ }
25+}
26+
27+export const usePals = () => {
28+ const queryClient = useQueryClient();
29+ const { data: charges } = useQuery('charges', () => {
30+ return api.scry<ChargeUpdateInitial>(scryCharges);
31+ })
32+ const { data } = useQuery('pals', () => {
33+ return api.scry<PalsData>({
34+ app: 'pals',
35+ path: '/json'
36+ });
37+ });
38+ const { mutate: remove } = useMutation((pal: string) => {
39+ return api.poke<RemovePal>({
40+ app: 'pals',
41+ mark: 'pals-command',
42+ json: {
43+ part: {
44+ ship: pal,
45+ in: []
46+ }
47+ }
48+ })
49+ }, {
50+ onSuccess: () => {
51+ queryClient.invalidateQueries('pals');
52+ }
53+ })
54+
55+ return {
56+ installed: !charges ? true : 'pals' in charges.initial,
57+ incoming: data ? Object.keys(data.incoming) : [],
58+ outgoing: data ? data.outgoing : {},
59+ remove
60+ }
61+}
ui/tailwind.config.js+3−1
@@ -12,5 +12,7 @@ module.exports = {
12 }
12 }
13 },
13 },
14 screens: {},
14 screens: {},
15− plugins: []
15+ plugins: [
16+ require('tailwindcss-labeled-groups')(['1', '2', '3'])
17+ ]
16};
18};