diff --git a/README.md b/README.md index 6fa2fdf..97a9de5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -# Friend Crawler +# Solid Friend Crawler -[live version](https://friends.livegraph.org) +Live at [friends.livegraph.org](https://friends.livegraph.org) + +Visualize the network of friends on Solid + +The app starts at the logged user (and timbl on solidcommunity.net) and crawls the foaf:knows connections. ## TODO @@ -8,11 +12,12 @@ - [x] make people a variable size based on how many people know them - [ ] show clearly what are the directions of :knows - [x] show also who knows this person -- [ ] login for different pod providers +- [x] login for different pod providers - [x] faster (parallel) crawling - [x] search people - [ ] make it more accessible and easier to navigate -- [ ] highlight also people who know the person +- [x] highlight also people who know the person - [ ] highlight people whose button is crawled in PersonList - [ ] add custom starting point for crawling - [x] support extended profile (seeAlso, sameAs) +- [ ] figure hosting it as SPA with router diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 5b10feb..91f86ce 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -5,6 +5,7 @@ import { handleIncomingRedirect, } from '@inrupt/solid-client-authn-browser' import { SessionContext, SessionInfo } from '../contexts/session' +import LoginPrompt from './LoginPrompt' interface Props { className?: string @@ -32,14 +33,21 @@ const Login: React.FC = ( setLoading(false) }) }, [setInfo]) - const handleLogin = async () => { + + const handleLogin = async (oidcIssuer: string) => { setLoading(true) - await login({ - oidcIssuer: 'https://solidcommunity.net', - redirectUrl: window.location.href, - clientName: 'Friends Crawler', - }) - setLoading(false) + try { + await login({ + oidcIssuer, + redirectUrl: window.location.href, + clientName: 'Friends Crawler', + }) + } catch (error) { + alert(`Could not find a Solid Pod at ${oidcIssuer}`) + localStorage.removeItem('idp') + } finally { + setLoading(false) + } } const handleLogout = async () => { @@ -55,15 +63,15 @@ const Login: React.FC = ( } return loading ? ( - Loading + ) : info?.isLoggedIn ? ( ) : ( - + ) } diff --git a/src/components/LoginPrompt.tsx b/src/components/LoginPrompt.tsx new file mode 100644 index 0000000..283dcb6 --- /dev/null +++ b/src/components/LoginPrompt.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react' +import Modal from 'react-modal' + +interface Props { + onLogin: (oidcIssuer: string) => void +} + +const LoginPrompt: React.FC = ({ onLogin, ...props }: Props) => { + const [promptOpen, setPromptOpen] = useState(false) + const [idp, setIdp] = useState( + localStorage.getItem('idp') ?? 'https://solidcommunity.net', + ) + + const onSubmit: React.FormEventHandler = e => { + e.preventDefault() + localStorage.setItem('idp', idp) + onLogin(idp) + } + + const onChangeInput = (e: React.FormEvent) => { + e.preventDefault() + const newValue = e.currentTarget.value + setIdp(newValue) + } + + if (!promptOpen) { + return ( + <> + + + ) + } + + return ( + <> + setPromptOpen(false)} + contentLabel="Connect your Solid Pod" + overlayClassName={{ + base: 'modal modal-background is-active', + afterOpen: '', + beforeClose: '', + }} + className={{ + base: 'modal-content', + afterOpen: '', + beforeClose: '', + }} + closeTimeoutMS={50} + > + + +
+
+

+ Select your Solid identity provider +

+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + ) +} + +export default LoginPrompt diff --git a/src/components/PersonCard.tsx b/src/components/PersonCard.tsx index cc49352..31a43e8 100644 --- a/src/components/PersonCard.tsx +++ b/src/components/PersonCard.tsx @@ -12,16 +12,18 @@ const PersonCard = ({ person, knows, known, onSelectPerson }: Props) => { return (
-

+

{person.name || person.uri}

- + + +
{person.photo && (
diff --git a/src/components/VisualizationContainer.tsx b/src/components/VisualizationContainer.tsx index 0c27347..eccea62 100644 --- a/src/components/VisualizationContainer.tsx +++ b/src/components/VisualizationContainer.tsx @@ -40,7 +40,7 @@ const InfoContainer = ({ children }: ICProps) => ( overflowX: 'hidden', }} > -
+
{children} @@ -155,7 +155,7 @@ const transformLayout = ( } function nodeRadius(person: Person) { - let count = person.known?.size ?? 0 + let count = new Set([...(person?.known ?? new Set()), ...person.knows]).size count = count < 1 ? 1 : count return count ** 0.42 * 5 } @@ -165,7 +165,12 @@ const selectNodeDependencies = ( graph: PeopleGraph, ): string[] => { if (!selectedNodeUri) return [] - return Array.from(graph?.[selectedNodeUri]?.knows ?? new Set()) + return [ + ...new Set([ + ...(graph?.[selectedNodeUri]?.knows ?? new Set()), + ...(graph?.[selectedNodeUri]?.known ?? new Set()), + ]), + ] } const VisualizationContainer: React.FC = ({ @@ -266,15 +271,15 @@ const VisualizationContainer: React.FC = ({ const grid = transformGrid(matrix, basicGrid) - let person, knows, known + let person + let knows: Person[] = [] + let known: Person[] = [] if (selectedNode) { person = people[selectedNode] if (person) { - knows = Array.from(person.knows).map(f => people[f]) - if (person.known) { - known = Array.from(person.known).map(f => people[f]) - } + knows = [...person.knows].map(f => people[f]) + known = [...(person?.known ?? new Set())].map(f => people[f]) } } @@ -297,7 +302,7 @@ const VisualizationContainer: React.FC = ({ - {person && knows && known ? ( + {person ? ( => { data.friends = data.friends .concat(friends) .filter((a, i, data) => data.indexOf(a) === i) - if (!data.name) data.name = getTerm(person, foaf.name)?.value ?? '' + if (!data.name) + data.name = + getTerm(person, foaf.name)?.value ?? + getTerm(person, vcard.fn)?.value ?? + '' if (!data.photo) data.photo = getTerm(person, vcard.hasPhoto)?.value ?? diff --git a/tsconfig.json b/tsconfig.json index a273b0c..4bb67d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, + "downlevelIteration": true, "jsx": "react-jsx" }, "include": [