mirror of
https://github.com/tloncorp/landscape.git
synced 2024-11-28 03:43:38 +03:00
Add Get Apps screen, rename "Leap" to "AppSearch"
AppSearch is just for apps. Real Leap to come later. Make sure we respect calm settings for notifications. Create WayfindingAppLink component. Use hard coded list of apps for Get Apps.
This commit is contained in:
parent
5fafa7ac88
commit
d953ffe501
@ -16,10 +16,7 @@ import useKilnState from './state/kiln';
|
||||
import useContactState from './state/contact';
|
||||
import api from './state/api';
|
||||
import { useMedia } from './logic/useMedia';
|
||||
import {
|
||||
useSettingsState,
|
||||
useTheme,
|
||||
} from './state/settings';
|
||||
import { useCalm, useSettingsState, useTheme } from './state/settings';
|
||||
import { useBrowserId, useLocalState } from './state/local';
|
||||
import { ErrorAlert } from './components/ErrorAlert';
|
||||
import { useErrorHandler } from './logic/useErrorHandler';
|
||||
@ -56,10 +53,13 @@ const AppRoutes = () => {
|
||||
const { search } = useLocation();
|
||||
const handleError = useErrorHandler();
|
||||
const browserId = useBrowserId();
|
||||
const {
|
||||
display: { doNotDisturb },
|
||||
} = useSettingsState.getState();
|
||||
const { count, unreadNotifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if ('Notification' in window) {
|
||||
if ('Notification' in window && !doNotDisturb) {
|
||||
if (count > 0 && Notification.permission === 'granted') {
|
||||
unreadNotifications.forEach((bin) => {
|
||||
makeBrowserNotification(bin);
|
||||
@ -120,7 +120,7 @@ const AppRoutes = () => {
|
||||
useHarkState.getState().start();
|
||||
|
||||
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
|
||||
push('/leap/search');
|
||||
push('/search');
|
||||
});
|
||||
}),
|
||||
[]
|
||||
@ -129,7 +129,7 @@ const AppRoutes = () => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/perma" component={PermalinkRoutes} />
|
||||
<Route path={['/leap/:menu', '/']} component={Grid} />
|
||||
<Route path={['/:menu', '/']} component={Grid} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import { Dialog, DialogContent } from './Dialog';
|
||||
import { Button } from './Button';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { GroupLink } from './GroupLink';
|
||||
import WayfindingAppLink from './WayfindingAppLink';
|
||||
|
||||
interface Group {
|
||||
title: string;
|
||||
@ -14,14 +15,6 @@ interface Group {
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface App {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
color: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const groups: Record<string, Group> = {
|
||||
foundation: {
|
||||
title: 'Urbit Foundation',
|
||||
@ -46,35 +39,6 @@ const groups: Record<string, Group> = {
|
||||
},
|
||||
};
|
||||
|
||||
const AppLink = ({ link, title, description, image, color }: App) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{image !== '' ? (
|
||||
<img
|
||||
src={image}
|
||||
className="h-8 w-8 rounded"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-8 w-8 rounded" style={{ backgroundColor: color }} />
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold">{title}</span>
|
||||
{description && (
|
||||
<span className="text-sm font-semibold text-gray-400">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="alt-primary" as="a" href={link} target="_blank">
|
||||
Open App
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function LandscapeDescription() {
|
||||
const charges = useCharges();
|
||||
return (
|
||||
@ -92,26 +56,29 @@ function LandscapeDescription() {
|
||||
software developer, like “~paldev”.
|
||||
</p>
|
||||
<div className="mt-8 space-y-2">
|
||||
<AppLink
|
||||
<WayfindingAppLink
|
||||
title="Groups"
|
||||
description="Build or join Urbit-based communities"
|
||||
link="/apps/groups"
|
||||
image={charges.groups?.image || ''}
|
||||
color={charges.groups?.color || 'bg-gray'}
|
||||
installed={charges['groups'] ? true : false}
|
||||
/>
|
||||
<AppLink
|
||||
<WayfindingAppLink
|
||||
title="Talk"
|
||||
description="Simple instant messaging app"
|
||||
link="/apps/talk"
|
||||
image={charges.talk?.image || ''}
|
||||
color={charges.talk?.color || 'bg-blue'}
|
||||
installed={charges['talk'] ? true : false}
|
||||
/>
|
||||
<AppLink
|
||||
<WayfindingAppLink
|
||||
title="Terminal"
|
||||
description="Pop open the hood of your urbit"
|
||||
link="/apps/webterm"
|
||||
image={charges.webterm?.image || ''}
|
||||
color={charges.webterm?.color || 'bg-black'}
|
||||
installed={charges['terminal'] ? true : false}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="my-8 text-2xl font-bold">Where are the people?</h1>
|
||||
|
55
ui/src/components/WayfindingAppLink.tsx
Normal file
55
ui/src/components/WayfindingAppLink.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface WayfindingAppLinkProps {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string | null;
|
||||
color: string;
|
||||
link: string;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
const WayfindingAppLink = ({
|
||||
link,
|
||||
title,
|
||||
description,
|
||||
image = null,
|
||||
color,
|
||||
installed,
|
||||
}: WayfindingAppLinkProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{image !== null && image !== '' ? (
|
||||
<img
|
||||
src={image}
|
||||
className="h-8 w-8 rounded"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-8 w-8 rounded" style={{ backgroundColor: color }} />
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-gray-800">{title}</span>
|
||||
{description && (
|
||||
<span className="text-sm font-semibold text-gray-400">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{installed ? (
|
||||
<Button variant="alt-primary" as="a" href={link} target="_blank">
|
||||
Open App
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="alt-primary" as="a" href={link} target="_blank">
|
||||
Install App
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WayfindingAppLink;
|
21
ui/src/components/icons/MagnifyingGlass16Icon.tsx
Normal file
21
ui/src/components/icons/MagnifyingGlass16Icon.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function MagnifyingGlass16Icon(
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
className="fill-current"
|
||||
fillRule="evenodd"
|
||||
d="M6.5 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-5a5 5 0 0 0-4.172 7.757l-.535.536-2 2a1 1 0 1 0 1.414 1.414l2-2 .536-.535A5 5 0 1 0 9.5 2Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -8,14 +8,15 @@ import React, {
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useEffect
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { useDebounce } from '../logic/useDebounce';
|
||||
import { useErrorHandler } from '../logic/useErrorHandler';
|
||||
import { useMedia } from '../logic/useMedia';
|
||||
import { MenuState, useLeapStore } from './Nav';
|
||||
import { MenuState, useAppSearchStore } from './Nav';
|
||||
|
||||
function normalizePathEnding(path: string) {
|
||||
const end = path.length - 1;
|
||||
@ -37,7 +38,6 @@ type LeapProps = {
|
||||
menu: MenuState;
|
||||
dropdown: string;
|
||||
navOpen: boolean;
|
||||
systemMenuOpen: boolean;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
function normalizeMatchString(match: string, keepAltChars: boolean): string {
|
||||
@ -50,19 +50,21 @@ function normalizeMatchString(match: string, keepAltChars: boolean): string {
|
||||
return normalizedString;
|
||||
}
|
||||
|
||||
export const Leap = React.forwardRef(
|
||||
({ menu, dropdown, navOpen, systemMenuOpen, className }: LeapProps, ref) => {
|
||||
export const AppSearch = React.forwardRef(
|
||||
({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
|
||||
const { push } = useHistory();
|
||||
const location = useLocation();
|
||||
const isMobile = useMedia('(max-width: 639px)');
|
||||
const deskMatch = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>(
|
||||
`/leap/${menu}/:query?/(apps)?/:desk?`
|
||||
const deskMatch = useRouteMatch<{
|
||||
menu?: MenuState;
|
||||
query?: string;
|
||||
desk?: string;
|
||||
}>(`/${menu}/:query?/(apps)?/:desk?`);
|
||||
const appsMatch = useRouteMatch(
|
||||
`/${menu}/${deskMatch?.params.query}/apps`
|
||||
);
|
||||
const systemPrefMatch = useRouteMatch<{ submenu: string }>(`/leap/system-preferences/:submenu`);
|
||||
const appsMatch = useRouteMatch(`/leap/${menu}/${deskMatch?.params.query}/apps`);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useImperativeHandle(ref, () => inputRef.current);
|
||||
const { rawInput, selectedMatch, matches, selection, select } = useLeapStore();
|
||||
const { rawInput, selectedMatch, matches, selection, select } =
|
||||
useAppSearchStore();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
useEffect(() => {
|
||||
@ -77,16 +79,24 @@ export const Leap = React.forwardRef(
|
||||
useEffect(() => {
|
||||
const newMatch = getMatch(rawInput);
|
||||
if (newMatch && rawInput) {
|
||||
useLeapStore.setState({ selectedMatch: newMatch });
|
||||
useAppSearchStore.setState({ selectedMatch: newMatch });
|
||||
}
|
||||
}, [rawInput, matches]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menu === 'search') {
|
||||
inputRef.current?.focus();
|
||||
} else {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}, [menu]);
|
||||
|
||||
const toggleSearch = useCallback(() => {
|
||||
if (selection || menu === 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
push('/leap/search');
|
||||
push('/search');
|
||||
}, [selection, menu]);
|
||||
|
||||
const onFocus = useCallback(
|
||||
@ -119,7 +129,7 @@ export const Leap = React.forwardRef(
|
||||
.trim()
|
||||
.replace('%', '')
|
||||
.replace(/(~?[\w^_-]{3,13})\//, '$1/apps/$1/');
|
||||
push(`/leap/${menu}/${normalizedValue}`);
|
||||
push(`/${menu}/${normalizedValue}`);
|
||||
},
|
||||
[menu]
|
||||
);
|
||||
@ -130,7 +140,7 @@ export const Leap = React.forwardRef(
|
||||
return;
|
||||
}
|
||||
|
||||
useLeapStore.setState({ searchInput: input });
|
||||
useAppSearchStore.setState({ searchInput: input });
|
||||
navigateByInput(input);
|
||||
},
|
||||
300,
|
||||
@ -139,52 +149,12 @@ export const Leap = React.forwardRef(
|
||||
|
||||
const handleSearch = useCallback(debouncedSearch, [deskMatch]);
|
||||
|
||||
const matchSystemPrefs = useCallback(
|
||||
(target: string) => {
|
||||
if (isMobile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!systemPrefMatch && target === 'system-updates') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return systemPrefMatch?.params.submenu === target;
|
||||
},
|
||||
[location, systemPrefMatch]
|
||||
);
|
||||
|
||||
const getPlaceholder = () => {
|
||||
if (systemMenuOpen) {
|
||||
switch (true) {
|
||||
case matchSystemPrefs('system-updates'):
|
||||
return 'My Urbit: About';
|
||||
case matchSystemPrefs('help'):
|
||||
return 'My Urbit: Help';
|
||||
case matchSystemPrefs('security'):
|
||||
return 'My Urbit: Security';
|
||||
case matchSystemPrefs('notifications'):
|
||||
return 'My Urbit: Notifications';
|
||||
case matchSystemPrefs('privacy'):
|
||||
return 'My Urbit: Attention & Privacy';
|
||||
case matchSystemPrefs('appearance'):
|
||||
return 'My Urbit: Appearance';
|
||||
case matchSystemPrefs('shortcuts'):
|
||||
return 'My Urbit: Shortcuts';
|
||||
case matchSystemPrefs('interface'):
|
||||
return 'My Urbit: Interface Settings';
|
||||
default:
|
||||
return 'Settings';
|
||||
}
|
||||
}
|
||||
return 'Search';
|
||||
};
|
||||
|
||||
const onChange = useCallback(
|
||||
handleError((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
const isDeletion = (e.nativeEvent as InputEvent).inputType === 'deleteContentBackward';
|
||||
const isDeletion =
|
||||
(e.nativeEvent as InputEvent).inputType === 'deleteContentBackward';
|
||||
const inputMatch = getMatch(value);
|
||||
const matchValue = inputMatch?.value;
|
||||
|
||||
@ -192,16 +162,17 @@ export const Leap = React.forwardRef(
|
||||
inputRef.current.value = matchValue;
|
||||
const start = matchValue.startsWith(value)
|
||||
? value.length
|
||||
: matchValue.substring(0, matchValue.indexOf(value)).length + value.length;
|
||||
: matchValue.substring(0, matchValue.indexOf(value)).length +
|
||||
value.length;
|
||||
inputRef.current.setSelectionRange(start, matchValue.length);
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
rawInput: matchValue,
|
||||
selectedMatch: inputMatch
|
||||
selectedMatch: inputMatch,
|
||||
});
|
||||
} else {
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
rawInput: value,
|
||||
selectedMatch: matches[0]
|
||||
selectedMatch: matches[0],
|
||||
});
|
||||
}
|
||||
|
||||
@ -227,7 +198,7 @@ export const Leap = React.forwardRef(
|
||||
}
|
||||
|
||||
push(currentMatch.url);
|
||||
useLeapStore.setState({ rawInput: '' });
|
||||
useAppSearchStore.setState({ rawInput: '' });
|
||||
}),
|
||||
[deskMatch, selectedMatch]
|
||||
);
|
||||
@ -239,7 +210,12 @@ export const Leap = React.forwardRef(
|
||||
|
||||
if (deletion && !rawInput && selection) {
|
||||
e.preventDefault();
|
||||
select(null, appsMatch && !appsMatch.isExact ? undefined : deskMatch?.params.query);
|
||||
select(
|
||||
null,
|
||||
appsMatch && !appsMatch.isExact
|
||||
? undefined
|
||||
: deskMatch?.params.query
|
||||
);
|
||||
const pathBack = createPreviousPath(deskMatch?.url || '');
|
||||
push(pathBack);
|
||||
}
|
||||
@ -257,14 +233,15 @@ export const Leap = React.forwardRef(
|
||||
return matchValue === searchValue;
|
||||
})
|
||||
: 0;
|
||||
const unsafeIndex = e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1;
|
||||
const unsafeIndex =
|
||||
e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1;
|
||||
const index = (unsafeIndex + matches.length) % matches.length;
|
||||
|
||||
const newMatch = matches[index];
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
rawInput: newMatch.value,
|
||||
// searchInput: matchValue,
|
||||
selectedMatch: newMatch
|
||||
selectedMatch: newMatch,
|
||||
});
|
||||
}
|
||||
}),
|
||||
@ -302,7 +279,7 @@ export const Leap = React.forwardRef(
|
||||
id="leap"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
placeholder={selection ? '' : getPlaceholder()}
|
||||
placeholder={selection ? '' : 'e.g., ~paldev or ~paldev/pals'}
|
||||
// TODO: style placeholder text with 100% opacity.
|
||||
// Not immediately clear how to do this within tailwind.
|
||||
className="outline-none h-full w-full flex-1 bg-transparent px-2 text-lg text-gray-800 sm:text-base"
|
||||
@ -320,7 +297,7 @@ export const Leap = React.forwardRef(
|
||||
</form>
|
||||
{menu === 'search' && (
|
||||
<Link
|
||||
to="/"
|
||||
to="/get-apps"
|
||||
className="circle-button default-ring absolute top-1/2 right-2 h-8 w-8 flex-none -translate-y-1/2 text-gray-600"
|
||||
onClick={() => select(null)}
|
||||
>
|
160
ui/src/nav/GetApps.tsx
Normal file
160
ui/src/nav/GetApps.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import WayfindingAppLink from '../components/WayfindingAppLink';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { AppSearch } from './AppSearch';
|
||||
import { MenuState} from './Nav';
|
||||
|
||||
const SECTIONS = {
|
||||
SELECTS: 'Tlon Selects',
|
||||
PALS: 'Powered by Pals',
|
||||
DEV: 'Develop on Urbit',
|
||||
USEFUL: 'Make Urbit Useful',
|
||||
};
|
||||
|
||||
const APPS = [
|
||||
{
|
||||
title: 'pals',
|
||||
description: 'friendlist for peer discovery',
|
||||
color: '#99D3BD',
|
||||
link: '/apps/pals',
|
||||
source: '~paldev',
|
||||
section: SECTIONS.SELECTS,
|
||||
desk: 'pals',
|
||||
},
|
||||
{
|
||||
title: 'Terminal',
|
||||
description: "A web interface to your Urbit's command line",
|
||||
color: '#233D34',
|
||||
link: '/apps/webterm',
|
||||
section: SECTIONS.SELECTS,
|
||||
desk: 'webterm',
|
||||
},
|
||||
{
|
||||
title: 'face',
|
||||
description: 'see your friends',
|
||||
color: '#3B5998',
|
||||
link: '/apps/face',
|
||||
section: SECTIONS.PALS,
|
||||
desk: 'face',
|
||||
},
|
||||
{
|
||||
title: 'rumors',
|
||||
description: 'Anonymous gossip from friends of friends',
|
||||
color: '#BB77DD',
|
||||
link: '/apps/rumors',
|
||||
section: SECTIONS.PALS,
|
||||
desk: 'rumors',
|
||||
},
|
||||
{
|
||||
title: 'Contacts',
|
||||
description: 'Contact book',
|
||||
color: '#338899',
|
||||
link: '/apps/whom',
|
||||
section: SECTIONS.PALS,
|
||||
desk: 'whom',
|
||||
},
|
||||
{
|
||||
title: "sc'o're",
|
||||
description: "leaderboard for groups' ['o']s, as seen on tv!",
|
||||
color: '#FFFF00',
|
||||
link: '/apps/scooore',
|
||||
section: SECTIONS.PALS,
|
||||
desk: 'scooore',
|
||||
},
|
||||
{
|
||||
title: 'Docs',
|
||||
description: 'User and developer documentation for Urbit apps',
|
||||
color: '#FFCF00',
|
||||
link: '/apps/docs',
|
||||
section: SECTIONS.DEV,
|
||||
desk: 'docs',
|
||||
},
|
||||
{
|
||||
title: 'Quorum',
|
||||
description:
|
||||
'A choral explanations app (a la Stack Overflow or Quora) for Urbit',
|
||||
color: '#F2F2F2',
|
||||
link: '/apps/quorom',
|
||||
section: SECTIONS.DEV,
|
||||
desk: 'quorum',
|
||||
image: 'https://ladrut.xyz/quorum/quorum-logo.png',
|
||||
},
|
||||
{
|
||||
title: 'silo',
|
||||
description: 'An S3 storage manager',
|
||||
color: '#4F46E5',
|
||||
link: '/apps/silo',
|
||||
section: SECTIONS.USEFUL,
|
||||
desk: 'silo',
|
||||
},
|
||||
{
|
||||
title: 'hodl',
|
||||
description: 'A portfolio for all that you hodl',
|
||||
color: '#B8A3D1',
|
||||
link: '/apps/hodl',
|
||||
section: SECTIONS.USEFUL,
|
||||
desk: 'hodl',
|
||||
image:
|
||||
'https://user-images.githubusercontent.com/16504501/194947852-8802fd63-5954-4ce8-b147-2072bd929242.png',
|
||||
},
|
||||
];
|
||||
|
||||
export default function GetApps() {
|
||||
const charges = useCharges();
|
||||
const { menu } = useParams<{ menu?: MenuState }>();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const menuState = menu || 'closed';
|
||||
const isOpen = menuState !== 'upgrading' && menuState !== 'closed';
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 overflow-y-scroll p-8">
|
||||
<h1 className="text-xl font-bold text-gray-800">Find Urbit Apps</h1>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h2 className="font-semibold text-gray-800">
|
||||
Find Urbit App Developers
|
||||
</h2>
|
||||
<AppSearch
|
||||
ref={inputRef}
|
||||
menu={menuState}
|
||||
dropdown="leap-items"
|
||||
navOpen={isOpen}
|
||||
/>
|
||||
</div>
|
||||
{Object.entries(SECTIONS).map(([key, name]) => (
|
||||
<div key={key} className="flex flex-col space-y-2">
|
||||
<h2 className="text-lg font-bold text-gray-800">{name}</h2>
|
||||
<div className="flex flex-col space-y-2 px-2">
|
||||
{APPS.map((app) => {
|
||||
if (app.section === name) {
|
||||
return (
|
||||
<WayfindingAppLink
|
||||
key={app.desk}
|
||||
title={
|
||||
charges[app.desk] ? charges[app.desk].title : app.title
|
||||
}
|
||||
description={
|
||||
charges[app.desk]
|
||||
? charges[app.desk]?.info ?? app.description
|
||||
: app.description
|
||||
}
|
||||
color={
|
||||
charges[app.desk] ? charges[app.desk].color : app.color
|
||||
}
|
||||
image={
|
||||
charges[app.desk]
|
||||
? charges[app.desk].image
|
||||
: app.image ?? ''
|
||||
}
|
||||
link={charges[app.desk] ? app.link : ''}
|
||||
installed={charges[app.desk] ? true : false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -22,7 +22,7 @@ import { Avatar } from '../components/Avatar';
|
||||
import { Dialog } from '../components/Dialog';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
import { Help } from './Help';
|
||||
import { Leap } from './Leap';
|
||||
import { AppSearch } from './AppSearch';
|
||||
import { Notifications } from './notifications/Notifications';
|
||||
import { NotificationsLink } from './notifications/NotificationsLink';
|
||||
import { Search } from './Search';
|
||||
@ -30,6 +30,8 @@ import { SystemPreferences } from '../preferences/SystemPreferences';
|
||||
import { useSystemUpdate } from '../logic/useSystemUpdate';
|
||||
import { Bullet } from '../components/icons/Bullet';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import MagnifyingGlass16Icon from '../components/icons/MagnifyingGlass16Icon';
|
||||
import GetApps from './GetApps';
|
||||
|
||||
export interface MatchItem {
|
||||
url: string;
|
||||
@ -38,7 +40,7 @@ export interface MatchItem {
|
||||
display?: string;
|
||||
}
|
||||
|
||||
interface LeapStore {
|
||||
interface AppSearchStore {
|
||||
rawInput: string;
|
||||
searchInput: string;
|
||||
matches: MatchItem[];
|
||||
@ -47,7 +49,7 @@ interface LeapStore {
|
||||
select: (selection: React.ReactNode, input?: string) => void;
|
||||
}
|
||||
|
||||
export const useLeapStore = create<LeapStore>((set) => ({
|
||||
export const useAppSearchStore = create<AppSearchStore>((set) => ({
|
||||
rawInput: '',
|
||||
searchInput: '',
|
||||
matches: [],
|
||||
@ -61,7 +63,7 @@ export const useLeapStore = create<LeapStore>((set) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
window.leap = useLeapStore.getState;
|
||||
window.appSearch = useAppSearchStore.getState;
|
||||
|
||||
export type MenuState =
|
||||
| 'closed'
|
||||
@ -99,7 +101,7 @@ export const SystemPrefsLink = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to="/leap/system-preferences" className="relative flex-none">
|
||||
<Link to="/system-preferences" className="relative flex-none">
|
||||
<Avatar shipName={window.ship} size="nav" />
|
||||
{systemBlocked && (
|
||||
<Bullet
|
||||
@ -111,15 +113,28 @@ export const SystemPrefsLink = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const GetAppsLink = ({ menuState }: PrefsLinkProps) => {
|
||||
const active = ['get-apps'].indexOf(menuState) >= 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/get-apps"
|
||||
className="flex h-9 w-[150px] items-center justify-center space-x-2 rounded-lg bg-blue-soft px-3 py-2.5"
|
||||
>
|
||||
<MagnifyingGlass16Icon className="h-4 w-4 fill-current text-blue" />
|
||||
<span className="whitespace-nowrap text-blue">Get Urbit Apps</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
const { push } = useHistory();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const dialogNavRef = useRef<HTMLDivElement>(null);
|
||||
const systemMenuOpen = useRouteMatch('/leap/system-preferences');
|
||||
const { systemBlocked } = useSystemUpdate();
|
||||
const [dialogContentOpen, setDialogContentOpen] = useState(false);
|
||||
const select = useLeapStore((state) => state.select);
|
||||
const select = useAppSearchStore((state) => state.select);
|
||||
|
||||
const menuState = menu || 'closed';
|
||||
const isOpen = menuState !== 'upgrading' && menuState !== 'closed';
|
||||
@ -172,13 +187,16 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
navOpen={isOpen}
|
||||
notificationsOpen={menu === 'notifications'}
|
||||
/>
|
||||
<Leap
|
||||
ref={inputRef}
|
||||
menu={menuState}
|
||||
dropdown="leap-items"
|
||||
navOpen={isOpen}
|
||||
systemMenuOpen={!!systemMenuOpen}
|
||||
/>
|
||||
{menuState === 'search' ? (
|
||||
<AppSearch
|
||||
ref={inputRef}
|
||||
menu={menuState}
|
||||
dropdown="leap-items"
|
||||
navOpen={isOpen}
|
||||
/>
|
||||
) : (
|
||||
<GetAppsLink menuState={menuState} />
|
||||
)}
|
||||
</Portal.Root>
|
||||
<div
|
||||
ref={navRef}
|
||||
@ -213,13 +231,11 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
role="listbox"
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/leap/notifications" component={Notifications} />
|
||||
<Route
|
||||
path="/leap/system-preferences"
|
||||
component={SystemPreferences}
|
||||
/>
|
||||
<Route path="/leap/help-and-support" component={Help} />
|
||||
<Route path={['/leap/search', '/leap']} component={Search} />
|
||||
<Route path="/notifications" component={Notifications} />
|
||||
<Route path="/system-preferences" component={SystemPreferences} />
|
||||
<Route path="/help-and-support" component={Help} />
|
||||
<Route path="/get-apps" component={GetApps} />
|
||||
<Route path={['/search']} component={Search} />
|
||||
</Switch>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
@ -9,7 +9,7 @@ import { usePike } from '../state/kiln';
|
||||
import { disableDefault, handleDropdownLink } from '../state/util';
|
||||
import { useMedia } from '../logic/useMedia';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { useLeapStore } from './Nav';
|
||||
import { useAppSearchStore } from './Nav';
|
||||
|
||||
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
open: boolean;
|
||||
@ -33,7 +33,7 @@ export const SystemMenu = ({
|
||||
const garden = usePike(window.desk);
|
||||
const hash = garden ? getHash(garden) : null;
|
||||
const isMobile = useMedia('(max-width: 639px)');
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const select = useAppSearchStore((s) => s.select);
|
||||
const clearSelection = useCallback(() => select(null), [select]);
|
||||
|
||||
const copyHash = useCallback(
|
||||
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Cross } from '../../components/icons/Cross';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useAppSearchStore } from '../Nav';
|
||||
import { SettingsState, useSettingsState } from '../../state/settings';
|
||||
import BellIcon from '../../components/icons/BellIcon';
|
||||
import { useNotifications } from './useNotifications';
|
||||
@ -32,12 +32,12 @@ export const NotificationsLink = ({ navOpen, notificationsOpen }: NotificationsL
|
||||
const dnd = useSettingsState(selDnd);
|
||||
const { count } = useNotifications();
|
||||
const state = getNotificationsState(notificationsOpen, count, dnd);
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const select = useAppSearchStore((s) => s.select);
|
||||
const clearSelection = useCallback(() => select(null), [select]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={state === 'open' ? '/' : '/leap/notifications'}
|
||||
to={state === 'open' ? '/' : '/notifications'}
|
||||
className={classNames(
|
||||
'relative z-50 flex-none circle-button h4 default-ring',
|
||||
navOpen && 'text-opacity-60',
|
||||
|
@ -4,7 +4,7 @@ import fuzzy from 'fuzzy';
|
||||
import { Treaty } from '@urbit/api';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import { useAllyTreaties } from '../../state/docket';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useAppSearchStore } from '../Nav';
|
||||
import { AppList } from '../../components/AppList';
|
||||
import { addRecentDev } from './Home';
|
||||
import { Spinner } from '../../components/Spinner';
|
||||
@ -12,7 +12,7 @@ import { Spinner } from '../../components/Spinner';
|
||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||
|
||||
export const Apps = ({ match }: AppsProps) => {
|
||||
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
|
||||
const { searchInput, selectedMatch, select } = useAppSearchStore((state) => ({
|
||||
searchInput: state.searchInput,
|
||||
select: state.select,
|
||||
selectedMatch: state.selectedMatch
|
||||
@ -61,7 +61,7 @@ export const Apps = ({ match }: AppsProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (results) {
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
matches: results.map((r) => ({
|
||||
url: getAppPath(r),
|
||||
openInNewTab: false,
|
||||
|
@ -3,7 +3,7 @@ import create from 'zustand';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { MatchItem, useLeapStore } from '../Nav';
|
||||
import { MatchItem, useAppSearchStore } from '../Nav';
|
||||
import { providerMatch } from './Providers';
|
||||
import { AppList } from '../../components/AppList';
|
||||
import { ProviderList } from '../../components/ProviderList';
|
||||
@ -88,7 +88,7 @@ function getApps(desks: string[], charges: ChargesWithDesks) {
|
||||
}
|
||||
|
||||
export const Home = () => {
|
||||
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||
const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
|
||||
const { recentApps, recentDevs } = useRecentsStore();
|
||||
const charges = useCharges();
|
||||
const groups = charges?.landscape;
|
||||
@ -108,7 +108,7 @@ export const Home = () => {
|
||||
}));
|
||||
const devs = recentDevs.map(providerMatch);
|
||||
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
matches: ([] as MatchItem[]).concat(appMatches, devs)
|
||||
});
|
||||
}, [recentApps, recentDevs]);
|
||||
|
@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
|
||||
import fuzzy from 'fuzzy';
|
||||
import { Provider, deSig } from '@urbit/api';
|
||||
import * as ob from 'urbit-ob';
|
||||
import { MatchItem, useLeapStore } from '../Nav';
|
||||
import { MatchItem, useAppSearchStore } from '../Nav';
|
||||
import { useAllies, useCharges } from '../../state/docket';
|
||||
import { ProviderList } from '../../components/ProviderList';
|
||||
import useContactState from '../../state/contact';
|
||||
@ -19,7 +19,7 @@ export function providerMatch(provider: Provider | string): MatchItem {
|
||||
return {
|
||||
value,
|
||||
display,
|
||||
url: `/leap/search/${value}/apps`,
|
||||
url: `/search/${value}/apps`,
|
||||
openInNewTab: false
|
||||
};
|
||||
}
|
||||
@ -34,7 +34,7 @@ function fuzzySort(search: string) {
|
||||
}
|
||||
|
||||
export const Providers = ({ match }: ProvidersProps) => {
|
||||
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||
const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
|
||||
const provider = match?.params.ship;
|
||||
const contacts = useContactState((s) => s.contacts);
|
||||
const charges = useCharges();
|
||||
@ -87,7 +87,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
useLeapStore.setState({ rawInput: search });
|
||||
useAppSearchStore.setState({ rawInput: search });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -106,7 +106,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
const newProviderMatches = isValidPatp
|
||||
? [
|
||||
{
|
||||
url: `/leap/search/${patp}/apps`,
|
||||
url: `/search/${patp}/apps`,
|
||||
value: patp,
|
||||
display: patp,
|
||||
openInNewTab: false
|
||||
@ -114,7 +114,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
]
|
||||
: [];
|
||||
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
matches: ([] as MatchItem[]).concat(appMatches, providerMatches, newProviderMatches)
|
||||
});
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ import { Spinner } from '../../components/Spinner';
|
||||
import useDocketState, { useCharge, useTreaty } from '../../state/docket';
|
||||
import { usePike } from '../../state/kiln';
|
||||
import { getAppName } from '../../state/util';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useAppSearchStore } from '../Nav';
|
||||
|
||||
export const TreatyInfo = () => {
|
||||
const select = useLeapStore((state) => state.select);
|
||||
const select = useAppSearchStore((state) => state.select);
|
||||
const { host, desk } = useParams<{ host: string; desk: string }>();
|
||||
const treaty = useTreaty(host, desk);
|
||||
const pike = usePike(desk);
|
||||
@ -23,7 +23,7 @@ export const TreatyInfo = () => {
|
||||
|
||||
useEffect(() => {
|
||||
select(<>{name}</>);
|
||||
useLeapStore.setState({ matches: [] });
|
||||
useAppSearchStore.setState({ matches: [] });
|
||||
}, [name]);
|
||||
|
||||
if (!treaty) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useLeapStore } from './nav/Nav';
|
||||
import { useAppSearchStore } from './nav/Nav';
|
||||
import { useRecentsStore } from './nav/search/Home';
|
||||
import useDocketState from './state/docket';
|
||||
|
||||
@ -8,7 +8,7 @@ declare global {
|
||||
desk: string;
|
||||
recents: typeof useRecentsStore.getState;
|
||||
docket: typeof useDocketState.getState;
|
||||
leap: typeof useLeapStore.getState;
|
||||
appSearch: typeof useAppSearchStore.getState;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ const base = new Theme().addColors({
|
||||
},
|
||||
blue: {
|
||||
DEFAULT: '#008EFF',
|
||||
soft: '#E5F4FF',
|
||||
50: '#EFF9FF',
|
||||
100: '#C8EDFF',
|
||||
200: '#A0E1FF',
|
||||
|
Loading…
Reference in New Issue
Block a user