mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-04 13:19:48 +03:00
Merge branch 'hm/grid-recent-lists' into hm/grid-system-update-flow
This commit is contained in:
commit
aaaee41e95
53
pkg/grid/src/components/AppLink.tsx
Normal file
53
pkg/grid/src/components/AppLink.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, LinkProps } from 'react-router-dom';
|
||||||
|
import { Docket } from '@urbit/api';
|
||||||
|
import { getAppHref } from '../state/util';
|
||||||
|
|
||||||
|
export type AppLinkProps = Omit<LinkProps, 'to'> & {
|
||||||
|
app: Docket;
|
||||||
|
small?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
to?: (app: Docket) => LinkProps['to'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppLink = ({
|
||||||
|
app,
|
||||||
|
to,
|
||||||
|
small = false,
|
||||||
|
selected = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AppLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={(to && to(app)) || getAppHref(app.href)}
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
||||||
|
selected && 'ring-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'flex-none relative bg-gray-200 rounded-lg',
|
||||||
|
small ? 'w-8 h-8' : 'w-12 h-12'
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: app.color }}
|
||||||
|
>
|
||||||
|
{app.image && (
|
||||||
|
<img
|
||||||
|
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||||
|
src={app.image}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-black">
|
||||||
|
<p>{app.title}</p>
|
||||||
|
{app.info && !small && <p className="font-normal">{app.info}</p>}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
46
pkg/grid/src/components/AppList.tsx
Normal file
46
pkg/grid/src/components/AppList.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { MouseEvent, useCallback } from 'react';
|
||||||
|
import { Docket } from '@urbit/api';
|
||||||
|
import { MatchItem } from '../nav/Nav';
|
||||||
|
import { useRecentsStore } from '../nav/search/Home';
|
||||||
|
import { AppLink, AppLinkProps } from './AppLink';
|
||||||
|
|
||||||
|
type AppListProps = {
|
||||||
|
apps: Docket[];
|
||||||
|
labelledBy: string;
|
||||||
|
matchAgainst?: MatchItem;
|
||||||
|
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: Docket) => void;
|
||||||
|
} & Omit<AppLinkProps, 'app' | 'onClick'>;
|
||||||
|
|
||||||
|
export function appMatches(target: Docket, match?: MatchItem): boolean {
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchValue = match.display || match.value;
|
||||||
|
return target.title === matchValue; // TODO: need desk name or something || target.href === matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppList = ({ apps, labelledBy, matchAgainst, onClick, ...props }: AppListProps) => {
|
||||||
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
|
const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="space-y-8" aria-labelledby={labelledBy}>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<li key={app.title} id={app.title} role="option" aria-selected={selected(app)}>
|
||||||
|
<AppLink
|
||||||
|
{...props}
|
||||||
|
app={app}
|
||||||
|
selected={selected(app)}
|
||||||
|
onClick={(e) => {
|
||||||
|
addRecentApp(app);
|
||||||
|
if (onClick) {
|
||||||
|
onClick(e, app);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
46
pkg/grid/src/components/ProviderLink.tsx
Normal file
46
pkg/grid/src/components/ProviderLink.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, LinkProps } from 'react-router-dom';
|
||||||
|
import { Provider } from '@urbit/api';
|
||||||
|
import { ShipName } from './ShipName';
|
||||||
|
|
||||||
|
export type ProviderLinkProps = Omit<LinkProps, 'to'> & {
|
||||||
|
provider: Provider;
|
||||||
|
small?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
to?: (p: Provider) => LinkProps['to'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProviderLink = ({
|
||||||
|
provider,
|
||||||
|
to,
|
||||||
|
selected = false,
|
||||||
|
small = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ProviderLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={(to && to(provider)) || `/leap/search/${provider.shipName}/apps`}
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
||||||
|
selected && 'ring-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'flex-none relative bg-black rounded-lg',
|
||||||
|
small ? 'w-8 h-8' : 'w-12 h-12'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* TODO: Handle sigils */}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-black">
|
||||||
|
<p className="font-mono">{provider.nickname || <ShipName name={provider.shipName} />}</p>
|
||||||
|
{provider.status && !small && <p className="font-normal">{provider.status}</p>}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
55
pkg/grid/src/components/ProviderList.tsx
Normal file
55
pkg/grid/src/components/ProviderList.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React, { MouseEvent, useCallback } from 'react';
|
||||||
|
import { Provider } from '@urbit/api';
|
||||||
|
import { MatchItem } from '../nav/Nav';
|
||||||
|
import { useRecentsStore } from '../nav/search/Home';
|
||||||
|
import { ProviderLink, ProviderLinkProps } from './ProviderLink';
|
||||||
|
|
||||||
|
export type ProviderListProps = {
|
||||||
|
providers: Provider[];
|
||||||
|
labelledBy: string;
|
||||||
|
matchAgainst?: MatchItem;
|
||||||
|
onClick?: (e: MouseEvent<HTMLAnchorElement>, p: Provider) => void;
|
||||||
|
} & Omit<ProviderLinkProps, 'provider' | 'onClick'>;
|
||||||
|
|
||||||
|
export function providerMatches(target: Provider, match?: MatchItem): boolean {
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchValue = match.display || match.value;
|
||||||
|
return target.nickname === matchValue || target.shipName === matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProviderList = ({
|
||||||
|
providers,
|
||||||
|
labelledBy,
|
||||||
|
matchAgainst,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: ProviderListProps) => {
|
||||||
|
const addRecentDev = useRecentsStore((state) => state.addRecentDev);
|
||||||
|
const selected = useCallback(
|
||||||
|
(provider: Provider) => providerMatches(provider, matchAgainst),
|
||||||
|
[matchAgainst]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="space-y-8" aria-labelledby={labelledBy}>
|
||||||
|
{providers.map((p) => (
|
||||||
|
<li key={p.shipName} id={p.shipName} role="option" aria-selected={selected(p)}>
|
||||||
|
<ProviderLink
|
||||||
|
{...props}
|
||||||
|
provider={p}
|
||||||
|
selected={selected(p)}
|
||||||
|
onClick={(e) => {
|
||||||
|
addRecentDev(p);
|
||||||
|
if (onClick) {
|
||||||
|
onClick(e, p);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
@ -5,16 +5,20 @@ type ShipNameProps = {
|
|||||||
} & HTMLAttributes<HTMLSpanElement>;
|
} & HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
export const ShipName = ({ name, ...props }: ShipNameProps) => {
|
export const ShipName = ({ name, ...props }: ShipNameProps) => {
|
||||||
const parts = name.replace('~', '').split(/[_^-]/);
|
const parts = name.replace('~', '').split(/([_^-])/);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...props}>
|
<span {...props}>
|
||||||
<span aria-hidden>~</span>
|
<span aria-hidden>~</span>
|
||||||
{/* <span className="sr-only">sig</span> */}
|
{/* <span className="sr-only">sig</span> */}
|
||||||
<span>{parts[0]}</span>
|
<span>{parts[0]}</span>
|
||||||
<span aria-hidden>-</span>
|
{parts.length > 1 && (
|
||||||
{/* <span className="sr-only">hep</span> */}
|
<>
|
||||||
<span>{parts[1]}</span>
|
<span aria-hidden>{parts[1]}</span>
|
||||||
|
{/* <span className="sr-only">hep</span> */}
|
||||||
|
<span>{parts[2]}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -136,7 +136,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = [slugify(getMatch(value)?.value || value)];
|
const input = [getMatch(value)?.value || slugify(value)];
|
||||||
if (appsMatch) {
|
if (appsMatch) {
|
||||||
input.unshift(match?.params.query || '');
|
input.unshift(match?.params.query || '');
|
||||||
} else {
|
} else {
|
||||||
|
@ -11,8 +11,10 @@ import { TreatyMeta } from '../../components/TreatyMeta';
|
|||||||
import useDocketState, { useCharges, useTreaty } from '../../state/docket';
|
import useDocketState, { useCharges, useTreaty } from '../../state/docket';
|
||||||
import { getAppHref } from '../../state/util';
|
import { getAppHref } from '../../state/util';
|
||||||
import { useLeapStore } from '../Nav';
|
import { useLeapStore } from '../Nav';
|
||||||
|
import { useRecentsStore } from './Home';
|
||||||
|
|
||||||
export const AppInfo = () => {
|
export const AppInfo = () => {
|
||||||
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
const select = useLeapStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
const { ship, host, desk } = useParams<{ ship: string; host: string; desk: string }>();
|
const { ship, host, desk } = useParams<{ ship: string; host: string; desk: string }>();
|
||||||
const treaty = useTreaty(host, desk);
|
const treaty = useTreaty(host, desk);
|
||||||
@ -55,6 +57,7 @@ export const AppInfo = () => {
|
|||||||
as="a"
|
as="a"
|
||||||
href={getAppHref(treaty.href)}
|
href={getAppHref(treaty.href)}
|
||||||
target={treaty.title || '_blank'}
|
target={treaty.title || '_blank'}
|
||||||
|
onClick={() => addRecentApp(treaty)}
|
||||||
>
|
>
|
||||||
Open App
|
Open App
|
||||||
</PillButton>
|
</PillButton>
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
import classNames from 'classnames';
|
import { Docket } from '@urbit/api/docket';
|
||||||
import { Treaty } from '@urbit/api/docket';
|
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { ShipName } from '../../components/ShipName';
|
||||||
import useDocketState, { useAllyTreaties } from '../../state/docket';
|
import useDocketState, { useAllyTreaties } from '../../state/docket';
|
||||||
import { useLeapStore } from '../Nav';
|
import { MatchItem, useLeapStore } from '../Nav';
|
||||||
|
import { AppList } from '../../components/AppList';
|
||||||
|
|
||||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
|
export function appMatch(app: Docket): MatchItem {
|
||||||
|
// TODO: do we need display vs value here,
|
||||||
|
// will all apps have unique titles? If not,
|
||||||
|
// what would we use?
|
||||||
|
return { value: app.title, display: app.title };
|
||||||
|
}
|
||||||
|
|
||||||
export const Apps = ({ match }: AppsProps) => {
|
export const Apps = ({ match }: AppsProps) => {
|
||||||
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
|
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
|
||||||
searchInput: state.searchInput,
|
searchInput: state.searchInput,
|
||||||
@ -49,7 +56,7 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (results) {
|
if (results) {
|
||||||
useLeapStore.setState({
|
useLeapStore.setState({
|
||||||
matches: results.map((treaty) => ({ value: treaty.desk, display: treaty.title }))
|
matches: results.map(appMatch)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [results]);
|
}, [results]);
|
||||||
@ -60,18 +67,6 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
}
|
}
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(target: Treaty) => {
|
|
||||||
if (!selectedMatch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchValue = selectedMatch.display || selectedMatch.value;
|
|
||||||
return target.title === matchValue || target.desk === matchValue;
|
|
||||||
},
|
|
||||||
[selectedMatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
|
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
|
||||||
<div id="developed-by">
|
<div id="developed-by">
|
||||||
@ -83,43 +78,12 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{results && (
|
{results && (
|
||||||
<ul className="space-y-8" aria-labelledby="developed-by">
|
<AppList
|
||||||
{results.map((treaty) => (
|
apps={results}
|
||||||
<li
|
labelledBy="developed-by"
|
||||||
key={treaty.desk}
|
matchAgainst={selectedMatch}
|
||||||
id={treaty.title || treaty.desk}
|
to={(app) => `${match?.path.replace(':ship', provider)}/${slugify(app.base)}`}
|
||||||
role="option"
|
/>
|
||||||
aria-selected={isSelected(treaty)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={`${match?.path.replace(':ship', provider)}/${treaty.ship}/${slugify(
|
|
||||||
treaty.desk
|
|
||||||
)}`}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
|
||||||
isSelected(treaty) && 'ring-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex-none relative w-12 h-12 bg-gray-200 rounded-lg"
|
|
||||||
style={{ backgroundColor: treaty.color }}
|
|
||||||
>
|
|
||||||
{treaty.image && (
|
|
||||||
<img
|
|
||||||
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
|
||||||
src={treaty.image}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-black">
|
|
||||||
<p>{treaty.title}</p>
|
|
||||||
{treaty.info && <p className="font-normal">{treaty.info}</p>}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
<p>That's it!</p>
|
<p>That's it!</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,35 +1,115 @@
|
|||||||
import { debounce } from 'lodash-es';
|
import produce from 'immer';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import create from 'zustand';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import React, { useEffect } from 'react';
|
||||||
import { createNextPath, useLeapStore } from '../Nav';
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { take } from 'lodash-es';
|
||||||
|
import { Docket, Provider } from '@urbit/api';
|
||||||
|
import { MatchItem, useLeapStore } from '../Nav';
|
||||||
|
import { appMatch } from './Apps';
|
||||||
|
import { providerMatch } from './Providers';
|
||||||
|
import { AppList } from '../../components/AppList';
|
||||||
|
import { ProviderList } from '../../components/ProviderList';
|
||||||
|
import { AppLink } from '../../components/AppLink';
|
||||||
|
import { ShipName } from '../../components/ShipName';
|
||||||
|
import { ProviderLink } from '../../components/ProviderLink';
|
||||||
|
import { useCharges } from '../../state/docket';
|
||||||
|
|
||||||
type HomeProps = RouteComponentProps;
|
interface RecentsStore {
|
||||||
|
recentApps: Docket[];
|
||||||
|
recentDevs: Provider[];
|
||||||
|
addRecentApp: (docket: Docket) => void;
|
||||||
|
addRecentDev: (dev: Provider) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const Home = ({ match, history }: HomeProps) => {
|
export const useRecentsStore = create<RecentsStore>(
|
||||||
const searchInput = useLeapStore((s) => s.searchInput);
|
persist(
|
||||||
const { push } = history;
|
(set) => ({
|
||||||
const { path } = match;
|
recentApps: [],
|
||||||
|
recentDevs: [],
|
||||||
|
addRecentApp: (docket) => {
|
||||||
|
set(
|
||||||
|
produce((draft: RecentsStore) => {
|
||||||
|
const hasApp = draft.recentApps.find((app) => app.href === docket.href);
|
||||||
|
if (!hasApp) {
|
||||||
|
draft.recentApps.unshift(docket);
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
draft.recentApps = take(draft.recentApps, 3);
|
||||||
debounce((input: string) => {
|
})
|
||||||
push(createNextPath(path, input.trim()));
|
);
|
||||||
}, 300),
|
},
|
||||||
[path]
|
addRecentDev: (dev) => {
|
||||||
);
|
set(
|
||||||
|
produce((draft: RecentsStore) => {
|
||||||
|
const hasDev = draft.recentDevs.find((p) => p.shipName === dev.shipName);
|
||||||
|
if (!hasDev) {
|
||||||
|
draft.recentDevs.unshift(dev);
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.recentDevs = take(draft.recentDevs, 3);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
whitelist: ['recentApps', 'recentDevs'],
|
||||||
|
name: 'recents-store'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Home = () => {
|
||||||
|
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||||
|
const { recentApps, recentDevs, addRecentApp, addRecentDev } = useRecentsStore();
|
||||||
|
const charges = useCharges();
|
||||||
|
const groups = charges?.groups;
|
||||||
|
const zod = { shipName: '~zod' };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchInput) {
|
const apps = recentApps.map(appMatch);
|
||||||
handleSearch(searchInput);
|
const devs = recentDevs.map(providerMatch);
|
||||||
}
|
|
||||||
}, [searchInput]);
|
useLeapStore.setState({
|
||||||
|
matches: ([] as MatchItem[]).concat(apps, devs)
|
||||||
|
});
|
||||||
|
}, [recentApps, recentDevs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full p-4 md:p-8 space-y-8 overflow-y-auto">
|
<div className="h-full p-4 md:p-8 space-y-8 font-semibold leading-tight text-black overflow-y-auto">
|
||||||
<h2 className="h4 text-gray-500">Recent Apps</h2>
|
<h2 id="recent-apps" className="h4 text-gray-500">
|
||||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
Recent Apps
|
||||||
|
</h2>
|
||||||
|
{recentApps.length === 0 && (
|
||||||
|
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
||||||
|
<p className="mb-4">Apps you use will be listed here, in the order you used them.</p>
|
||||||
|
<p className="mb-6">You can click/tap/keyboard on a listed app to open it.</p>
|
||||||
|
{groups && <AppLink app={groups} small onClick={() => addRecentApp(groups)} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{recentApps.length > 0 && (
|
||||||
|
<AppList apps={recentApps} labelledBy="recent-apps" matchAgainst={selectedMatch} small />
|
||||||
|
)}
|
||||||
<hr className="-mx-4 md:-mx-8" />
|
<hr className="-mx-4 md:-mx-8" />
|
||||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
<h2 id="recent-devs" className="h4 text-gray-500">
|
||||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
Recent Developers
|
||||||
|
</h2>
|
||||||
|
{recentDevs.length === 0 && (
|
||||||
|
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
||||||
|
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
|
||||||
|
<p className="mb-6">
|
||||||
|
Try out app discovery by visiting <ShipName name="~zod" /> below.
|
||||||
|
</p>
|
||||||
|
<ProviderLink provider={zod} small onClick={() => addRecentDev(zod)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{recentDevs.length > 0 && (
|
||||||
|
<ProviderList
|
||||||
|
providers={recentDevs}
|
||||||
|
labelledBy="recent-devs"
|
||||||
|
matchAgainst={selectedMatch}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
import classNames from 'classnames';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { Provider } from '@urbit/api';
|
||||||
import { useLeapStore } from '../Nav';
|
import { MatchItem, useLeapStore } from '../Nav';
|
||||||
import { useAllies } from '../../state/docket';
|
import { useAllies } from '../../state/docket';
|
||||||
|
import { ProviderList } from '../../components/ProviderList';
|
||||||
|
|
||||||
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
|
export function providerMatch(provider: Provider | string): MatchItem {
|
||||||
|
if (typeof provider === 'string') {
|
||||||
|
return { value: provider, display: provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: provider.shipName, display: provider.nickname };
|
||||||
|
}
|
||||||
|
|
||||||
export const Providers = ({ match }: ProvidersProps) => {
|
export const Providers = ({ match }: ProvidersProps) => {
|
||||||
const { selectedMatch, select } = useLeapStore((state) => ({
|
const { selectedMatch, select } = useLeapStore((state) => ({
|
||||||
select: state.select,
|
select: state.select,
|
||||||
@ -30,10 +38,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
|
|
||||||
return right - left;
|
return right - left;
|
||||||
})
|
})
|
||||||
.map((el) => {
|
.map((el) => ({ shipName: el.original }))
|
||||||
console.log(el);
|
|
||||||
return el.original;
|
|
||||||
})
|
|
||||||
: [],
|
: [],
|
||||||
[allies, search]
|
[allies, search]
|
||||||
);
|
);
|
||||||
@ -47,23 +52,11 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (results) {
|
if (results) {
|
||||||
useLeapStore.setState({
|
useLeapStore.setState({
|
||||||
matches: results.map((p) => ({ value: p, display: p }))
|
matches: results.map(providerMatch)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(target: string) => {
|
|
||||||
if (!selectedMatch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchValue = selectedMatch.display || selectedMatch.value;
|
|
||||||
return target === matchValue;
|
|
||||||
},
|
|
||||||
[selectedMatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
||||||
<div id="providers">
|
<div id="providers">
|
||||||
@ -73,28 +66,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{results && (
|
{results && (
|
||||||
<ul className="space-y-8" aria-labelledby="providers">
|
<ProviderList providers={results} labelledBy="providers" matchAgainst={selectedMatch} />
|
||||||
{results.map((p) => (
|
|
||||||
<li key={p} id={p} role="option" aria-selected={isSelected(p)}>
|
|
||||||
<Link
|
|
||||||
to={`${match?.path.replace(':ship', p)}/apps`}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
|
||||||
isSelected(p) && 'ring-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex-none relative w-12 h-12 bg-black rounded-lg">
|
|
||||||
{/* TODO: Handle sigils */}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-black">
|
|
||||||
<p className="font-mono">
|
|
||||||
<ShipName name={p} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
<p>That's it!</p>
|
<p>That's it!</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import { chadIsRunning, Charge } from '@urbit/api/docket';
|
|||||||
import { TileMenu } from './TileMenu';
|
import { TileMenu } from './TileMenu';
|
||||||
import { Spinner } from '../components/Spinner';
|
import { Spinner } from '../components/Spinner';
|
||||||
import { getAppHref } from '../state/util';
|
import { getAppHref } from '../state/util';
|
||||||
|
import { useRecentsStore } from '../nav/search/Home';
|
||||||
|
|
||||||
type TileProps = {
|
type TileProps = {
|
||||||
charge: Charge;
|
charge: Charge;
|
||||||
@ -24,6 +25,7 @@ function getMenuColor(color: string, lightText: boolean, active: boolean): strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
||||||
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
const { title, color, image, chad, href } = charge;
|
const { title, color, image, chad, href } = charge;
|
||||||
const loading = 'install' in chad;
|
const loading = 'install' in chad;
|
||||||
const active = chadIsRunning(chad);
|
const active = chadIsRunning(chad);
|
||||||
@ -42,6 +44,8 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
|||||||
!active && 'cursor-default'
|
!active && 'cursor-default'
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
|
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
|
||||||
|
onClick={() => addRecentApp(charge)}
|
||||||
|
onAuxClick={() => addRecentApp(charge)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
Loading…
Reference in New Issue
Block a user