Skip to content

Commit

Permalink
Get basic search UI up and running
Browse files Browse the repository at this point in the history
  • Loading branch information
ianconsolata committed Feb 20, 2023
1 parent ddaced8 commit 4cdd437
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 56 deletions.
14 changes: 7 additions & 7 deletions components/Dropdown.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Fragment } from 'react'
import { Menu, Transition } from '@headlessui/react'
import { AddCircle as AddCircleIcon } from './icons'
import { Fragment } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { AddCircle as AddCircleIcon } from './icons';

export default function Dropdown({children, label, className}) {
export default function Dropdown({ children, label, className }) {
return (
<Menu as="div" className={`relative inline-block text-left ${className}`}>
<div>
Expand All @@ -23,8 +23,8 @@ export default function Dropdown({children, label, className}) {
{children}
</Transition>
</Menu>
)
);
}

Dropdown.Items = Menu.Items
Dropdown.Item = Menu.Item
Dropdown.Items = Menu.Items;
Dropdown.Item = Menu.Item;
1 change: 0 additions & 1 deletion components/GardenHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ function GardenAuthor({ profile }) {

export default function GardenHeader({
type,
onSearch,
openSidebar,
authorProfile,
gardenSettings,
Expand Down
5 changes: 1 addition & 4 deletions components/ProfileDrawer/Webhooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,10 @@ async function ensureAcl(resourceUrl, options) {
let acl;
if (hasResourceAcl(resourceWithAcl)) {
acl = getResourceAcl(resourceWithAcl);
console.log(`getResourceAcl:${acl}`);
} else if (hasFallbackAcl(resourceWithAcl)) {
acl = createAclFromFallbackAcl(resourceWithAcl);
console.log(`createAclFromFallbackAcl:${acl}`);
} else {
acl = createAcl(resourceWithAcl);
console.log(`createAcl:${acl}`);
}
if (options.default) {
if (options.default.public) {
Expand All @@ -77,7 +74,7 @@ async function ensureAcl(resourceUrl, options) {
}
await saveAclFor(resourceWithAcl, acl, options);
} else {
throw new Error('Cannot ensureAcl for undefined resource');
throw new Error('Cannot ensureAcl for unknown resource');
}
}

Expand Down
136 changes: 122 additions & 14 deletions components/Search.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,128 @@
import { Formik } from 'formik';
import {
getTitle,
useGarden,
useSpaces,
gardenMetadataInSpacePrefs,
getSpace,
HomeSpaceSlug,
} from 'garden-kit';
import { useState, Fragment } from 'react';
import { useWebId } from 'swrlit';
import {
useCommunityContactsSearchResults,
useCommunityGardenSearchResults,
useGardenSearchResults,
} from '../hooks/search';
import { Search as SearchIcon } from './icons';
import { IconInput } from './inputs';
import { Combobox, Transition } from '@headlessui/react';
import { asUrl, getSourceUrl, getThing } from '@inrupt/solid-client';
import { classNames } from '../utils/html';
import { result } from 'rdf-namespaces/dist/as';

export function SearchResults({ title, results }) {
return (
<>
{results && results.length > 0 && (
<div className="uppercase text-gray-300 text-xs mt-2.5 px-4">
{title}
</div>
)}
{results &&
results.map((result) => (
<Combobox.Option key={result.item.uuid} value={result} as={Fragment}>
{({ active }) => (
<a
href="#"
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'menu-item'
)}
>
{result.item.title}
</a>
)}
</Combobox.Option>
))}
</>
);
}

export function CommunityGardenSearchResults({ search }) {
const results = useCommunityGardenSearchResults(search);
return <SearchResults title="Community Garden" results={results} />;
}

export function CommunityContactsSearchResults({ search }) {
const results = useCommunityContactsSearchResults(search);
return <SearchResults title="People" results={results} />;
}

export function GardenSearchResults({ search, gardenUrl }) {
const { garden } = useGarden(gardenUrl);
const gardenSettings = garden && getThing(garden, getSourceUrl(garden));
const results = useGardenSearchResults(search, garden);
if (gardenSettings)
return <SearchResults title={getTitle(gardenSettings)} results={results} />;
else return <></>;
}

export default function Search({}) {
const [search, setSearch] = useState('');

const webId = useWebId();
const { spaces } = useSpaces(webId);
const home = spaces && getSpace(spaces, HomeSpaceSlug);
const gardens = gardenMetadataInSpacePrefs(home, spaces);

const [selectedResult, setSelectedResult] = useState(undefined);

const selectSearchResult = (result) => {
console.log(result);
setSelectedResult(result);
};

return (
<Formik>
<IconInput
type="search"
name="search"
placeholder="Search"
icon={<SearchIcon className="ipt-header-search-icon" />}
inputClassName="ipt-header-search"
onChange={(e) => {
e.preventDefault();
}}
/>
</Formik>
<div className="flex flex-row max-h-9 self-center">
<div className="relative overflow-y-visible">
<Combobox value={selectedResult} onChange={selectSearchResult}>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<SearchIcon className="ipt-header-search-icon" />
</div>
<Combobox.Input
type="search"
name="search"
placeholder="Search"
className="pl-12 ipt-header-search"
displayValue={(result) => result.item.title}
onChange={(event) => setSearch(event.target.value)}
/>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Combobox.Options className="origin-top-left absolute right-0 mt-2 w-52 rounded-lg overflow-hidden shadow-menu text-xs bg-white focus:outline-none z-40">
{gardens &&
gardens.map((garden) => {
return (
<GardenSearchResults
search={search}
gardenUrl={asUrl(garden)}
/>
);
})}
<CommunityGardenSearchResults search={search} />
<CommunityContactsSearchResults search={search} />
</Combobox.Options>
</Transition>
</Combobox>
</div>
</div>
);
}
11 changes: 9 additions & 2 deletions hooks/community.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
toRdfJsDataset,
buildThing,
} from '@inrupt/solid-client';
import { useGarden } from 'garden-kit';
import { RDFS, DCTERMS, OWL } from '@inrupt/vocab-common-rdf';
import { useCallback } from 'react';
import { useResource, useThing } from 'swrlit';
import { addRDFType, isPerson, useGarden } from 'garden-kit';

export const CommunityNurseryUrl =
process.env.NEXT_PUBLIC_COMMUNITY_NURSERY_URL ||
Expand Down Expand Up @@ -65,12 +65,15 @@ export function usernameFromUrl(contactUrl) {
export function usernameFromContact(contact) {
return usernameFromUrl(asUrl(contact));
}

export function createContact(username, webId) {
return buildThing(createThing({ name: username }))
const contact = buildThing(createThing({ name: username }))
.addUrl(OWL.sameAs, webId)
.addDatetime(DCTERMS.modified, new Date())
.addDatetime(DCTERMS.created, new Date())
.build();

return addRDFType(contact, MY.Garden.Person);
}

export function addContact(contacts, username, webId) {
Expand All @@ -87,6 +90,10 @@ export function getContact(contacts, username) {
return getThing(contacts, urlForUsername(username));
}

export function getContactAll(contacts) {
return getThingAll(contacts).filter(isPerson);
}

export function getContactByWebId(contacts, webId) {
const dataset = contacts && toRdfJsDataset(contacts);
const matches = dataset && dataset.match(null, OWL.sameAs, webId);
Expand Down
89 changes: 65 additions & 24 deletions hooks/search.jsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,79 @@
import { getThingAll } from '@inrupt/solid-client';
import { FOAF } from '@inrupt/vocab-common-rdf';
import { useCommunityContacts, useCommunityGarden } from './community';
import {
getSourceUrl,
getStringNoLocale,
getThing,
getUrl,
} from '@inrupt/solid-client';
import { FOAF, OWL } from '@inrupt/vocab-common-rdf';
import { getItemAll, MY } from 'garden-kit';
import { useMemo } from 'react';
import useSWR from 'swr';
import { defaultOptions, fuseEntriesFromGardenItems } from '../model/search';
import {
getContactAll,
useCommunityContacts,
useCommunityGarden,
usernameFromContact,
} from './community';
import Fuse from 'fuse.js';

export function useCommunityContactsSearch(search) {
export function useCommunityContactsSearchResults(search) {
const { contacts } = useCommunityContacts();
const entries = getThingAll(contacts).map((contact) => {
return {
name: getStringNoLocale(contact, FOAF.name);
}
});
const contactsSettings =
contacts && getThing(contacts, getSourceUrl(contacts));
const fuseIndexUrl =
contactsSettings && getUrl(contactsSettings, MY.Garden.hasFuseIndex);
const entries =
contacts &&
getContactAll(contacts).map((contact) => {
return {
name: getStringNoLocale(contact, FOAF.name),
webId: getStringNoLocale(contact, OWL.sameAs),
usename: usernameFromContact(contact),
};
});

return useSearchResults(
search,
entries,
defaultOptions(['name', 'username']),
fuseIndexUrl
);
}

export function useGardenSearch(search, garden) {}
export function useCommunityGardenSearchResults(search) {
const { garden } = useCommunityGarden();
return useGardenSearchResults(search, garden);
}

export function useSpacesSearch(searc, spaces) {}
export function useGardenSearchResults(search, garden) {
const { entries, options } = garden
? fuseEntriesFromGardenItems(getItemAll(garden))
: { entries: undefined, options: undefined };
const gardenSettings = garden && getThing(garden, getSourceUrl(garden));
const fuseIndexUrl =
gardenSettings && getUrl(gardenSettings, MY.Garden.hasFuseIndex);

return useSearchResults(search, entries, options, fuseIndexUrl);
}

export function useFuse(search, entries, keys, fuseIndexFile) {
export function useSearchResults(search, entries, options, fuseIndexFile) {
const fetcher = (url) => fetch(url).then((res) => res.json());
const { data: fuseIndex } = useSWR(fuseIndexFile, fetcher);

const options = {
includeScore: true,
threshold: 0.3,
keys: keys,
};

const fuse = useMemo(() => {
if (!entries) return undefined;
if (fuseIndex) return new Fuse(entries, options, fuseIndex);
else return new Fuse(entries, options);
}, [entries, fuseIndex]);

return fuse.search(search);
}
}, [entries, options, fuseIndex]);

export function useSearch(search) {
const results = useMemo(() => {
if (fuse) {
return fuse.search(search);
} else {
return [];
}
}, [fuse, search]);

}
return results;
}
6 changes: 2 additions & 4 deletions model/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,8 @@ async function setupOrUpdateGardenSearchIndex(garden, fuseIndexUrl, { fetch }) {
}

const updatedIndex = fuse.getIndex();
const json = JSON.stringify(updatedIndex.toJSON());
console.log('Saving json index');
console.log(json);
const buf = Buffer.from(json);
console.log('Updating saved json index');
const buf = Buffer.from(JSON.stringify(updatedIndex.toJSON()));
await overwriteFile(fuseIndexUrl, buf, {
fetch,
contentType: 'application/json',
Expand Down

0 comments on commit 4cdd437

Please sign in to comment.