grid: refactor docket/treaty display

This commit is contained in:
Liam Fitzgerald 2021-08-25 17:41:31 +10:00
parent 4df4ef56d6
commit 93a9926ad0
7 changed files with 215 additions and 32 deletions

View File

@ -0,0 +1,131 @@
import { chadIsRunning, Charge, Treaty } from '@urbit/api/docket';
import clipboardCopy from 'clipboard-copy';
import React, { FC } from 'react';
import cn from 'classnames';
import { Vat } from '@urbit/api/hood';
import { Button, PillButton } from './Button';
import { Dialog, DialogClose, DialogContent, DialogTrigger } from './Dialog';
import { DocketHeader } from './DocketHeader';
import { Spinner } from './Spinner';
import { VatMeta } from './VatMeta';
import useDocketState from '../state/docket';
import { getAppHref } from '../state/util';
import { addRecentApp } from '../nav/search/Home';
import { TreatyMeta } from './TreatyMeta';
type InstallStatus = 'uninstalled' | 'installing' | 'installed';
interface AppInfoProps {
docket: Charge | Treaty;
vat?: Vat;
className?: string;
}
function getInstallStatus(docket: Treaty | Charge): InstallStatus {
if (!('chad' in docket)) {
return 'uninstalled';
}
if (chadIsRunning(docket.chad)) {
return 'installed';
}
if ('install' in docket.chad) {
return 'installing';
}
return 'uninstalled';
}
function getRemoteDesk(docket: Treaty | Charge, vat?: Vat) {
if ('chad' in docket) {
const { ship, desk } = vat!.arak;
return [ship, desk];
}
const { ship, desk } = docket;
return [ship, desk];
}
export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
const installStatus = getInstallStatus(docket);
const [ship, desk] = getRemoteDesk(docket, vat);
const installApp = async () => {
if (installStatus === 'installed') {
return;
}
await useDocketState.getState().installDocket(ship, desk);
};
const copyApp = () => {
clipboardCopy(`web+urbitgraph://app/${ship}/${desk}`);
};
if (!docket) {
// TODO: maybe replace spinner with skeletons
return (
<div className="dialog-inner-container text-black">
<span>Loading...</span>
</div>
);
}
return (
<div className={cn('text-black', className)}>
<DocketHeader docket={docket}>
<div className="col-span-2 md:col-span-1 flex items-center space-x-4">
{installStatus === 'installed' && (
<PillButton
variant="alt-primary"
as="a"
href={getAppHref(docket.href)}
target={docket.title || '_blank'}
onClick={() => addRecentApp(docket)}
>
Open App
</PillButton>
)}
{installStatus !== 'installed' && (
<Dialog>
<DialogTrigger as={PillButton} variant="alt-primary">
{installStatus === 'installing' ? (
<>
<Spinner />
<span className="sr-only">Installing...</span>
</>
) : (
'Get App'
)}
</DialogTrigger>
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
<h2 className="h4">Install &ldquo;{docket.title}&rdquo;</h2>
<p className="text-base tracking-tight pr-6">
This application will be able to view and interact with the contents of your
Urbit. Only install if you trust the developer.
</p>
<div className="flex space-x-6">
<DialogClose as={Button} variant="secondary">
Cancel
</DialogClose>
<DialogClose as={Button} onClick={installApp}>
Get &ldquo;{docket.title}&rdquo;
</DialogClose>
</div>
</DialogContent>
</Dialog>
)}
<PillButton variant="alt-secondary" onClick={copyApp}>
Copy App Link
</PillButton>
</div>
</DocketHeader>
{vat ? (
<>
<hr className="-mx-5 sm:-mx-8" />
<VatMeta vat={vat} />
</>
) : null}
{'chad' in docket ? null : (
<>
<hr className="-mx-5 sm:-mx-8" />
<TreatyMeta treaty={docket} />
</>
)}
</div>
);
};

View File

@ -1,16 +1,22 @@
import classNames from 'classnames';
import React from 'react';
import React, { HTMLProps, ReactNode } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { Docket } from '@urbit/api';
import { getAppHref } from '../state/util';
type Sizes = 'xs' | 'small' | 'default';
type LinkOrAnchorProps = {
[P in keyof LinkProps &
keyof HTMLProps<HTMLAnchorElement>]?: LinkProps[P] extends HTMLProps<HTMLAnchorElement>[P]
? LinkProps[P]
: never;
};
export type AppLinkProps = Omit<LinkProps, 'to'> & {
app: Docket;
export type AppLinkProps<T extends Docket> = Omit<LinkOrAnchorProps, 'to'> & {
app: T;
size?: Sizes;
selected?: boolean;
to?: (app: Docket) => LinkProps['to'];
to?: (app: T) => LinkProps['to'] | undefined;
};
const sizeMap: Record<Sizes, string> = {
@ -19,24 +25,32 @@ const sizeMap: Record<Sizes, string> = {
default: 'w-12 h-12 mr-3 rounded-lg'
};
export const AppLink = ({
export const AppLink = <T extends Docket>({
app,
to,
size = 'default',
selected = false,
className,
...props
}: AppLinkProps) => {
return (
<Link
to={(to && to(app)) || getAppHref(app.href)}
className={classNames(
}: AppLinkProps<T>) => {
const linkTo = to?.(app);
const linkClassnames = classNames(
'flex items-center default-ring ring-offset-2 rounded-lg',
selected && 'ring-4',
className
)}
{...props}
>
);
const link = (children: ReactNode) =>
linkTo ? (
<Link to={linkTo} className={linkClassnames} {...props}>
{children}
</Link>
) : (
<a href={getAppHref(app.href)} className={linkClassnames} {...props}>
{children}
</a>
);
return link(
<>
<div
className={classNames('flex-none relative bg-gray-200 rounded-lg', sizeMap[size])}
style={{ backgroundColor: app.color }}
@ -53,6 +67,6 @@ export const AppLink = ({
<p>{app.title}</p>
{app.info && size === 'default' && <p className="font-normal">{app.info}</p>}
</div>
</Link>
</>
);
};

View File

@ -4,13 +4,13 @@ import { MatchItem } from '../nav/Nav';
import { useRecentsStore } from '../nav/search/Home';
import { AppLink, AppLinkProps } from './AppLink';
type AppListProps = {
apps: Docket[];
type AppListProps<T extends Docket> = {
apps: T[];
labelledBy: string;
matchAgainst?: MatchItem;
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: Docket) => void;
listClass?: string;
} & Omit<AppLinkProps, 'app' | 'onClick'>;
} & Omit<AppLinkProps<T>, 'app' | 'onClick'>;
export function appMatches(target: Docket, match?: MatchItem): boolean {
if (!match) {
@ -21,14 +21,14 @@ export function appMatches(target: Docket, match?: MatchItem): boolean {
return target.title === matchValue; // TODO: need desk name or something || target.href === matchValue;
}
export const AppList = ({
export const AppList = <T extends Docket>({
apps,
labelledBy,
matchAgainst,
onClick,
listClass = 'space-y-8',
...props
}: AppListProps) => {
}: AppListProps<T>) => {
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]);

View File

@ -1,14 +1,16 @@
import React from 'react';
import cn from 'classnames';
import { capitalize } from 'lodash-es';
interface AttributeProps {
attr: string;
children: React.ReactNode;
title?: string;
className?: string;
}
export const Attribute = ({ attr, children, title }: AttributeProps) => (
<div className="h4">
export const Attribute = ({ attr, children, title, className }: AttributeProps) => (
<div className={cn('h4', className)}>
<h2 className="mb-2 text-gray-500">{title || capitalize(attr)}</h2>
<p className="font-mono">{children}</p>
</div>

View File

@ -3,18 +3,19 @@ import { Docket } from '@urbit/api/docket';
import cn from 'classnames';
interface DocketImageProps extends Pick<Docket, 'color' | 'image'> {
small?: boolean;
className?: string;
sizing: 'small' | 'full';
}
export function DocketImage({ color, image, className = '', small = false }: DocketImageProps) {
const sizing = small
? 'w-4 h-4 md:w-6 md:h-6 rounded-md'
export function DocketImage({ color, image, className = '', sizing = 'full' }: DocketImageProps) {
const sizingClass =
sizing === 'full'
? 'w-full h-full md:w-full md:h-full rounded-md'
: 'w-12 h-12 md:w-20 md:h-20 rounded-xl';
return (
<div
className={cn(sizing, `flex-none relative bg-gray-200`, className)}
className={cn(sizingClass, `flex-none relative bg-gray-200`, className)}
style={{ backgroundColor: color }}
>
{image && (

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Treaty } from '@urbit/api';
import { Treaty, daToDate } from '@urbit/api';
import moment from 'moment';
import { Attribute } from './Attribute';
const meta = ['license', 'website', 'version'] as const;
@ -8,13 +9,14 @@ const meta = ['license', 'website', 'version'] as const;
export function TreatyMeta(props: { treaty: Treaty }) {
const { treaty } = props;
const { desk, ship, cass } = treaty;
console.log(cass.da);
return (
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
<Attribute title="Developer Desk" attr="desk">
{ship}/{desk}
</Attribute>
<Attribute title="Last Software Update" attr="case">
{JSON.stringify(cass)}
{moment(daToDate(cass.da as unknown as string)).format('YYYY.MM.DD')}
</Attribute>
{meta.map((d) => (
<Attribute key={d} attr={d}>

View File

@ -0,0 +1,33 @@
import React from 'react';
import moment from 'moment';
import { Vat } from '@urbit/api/hood';
import { Attribute } from './Attribute';
export function VatMeta(props: { vat: Vat }) {
const { vat } = props;
const { desk, arak, cass, hash } = vat;
const { desk: foreignDesk, ship, next } = arak;
const pluralUpdates = next.length !== 1;
return (
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
<Attribute title="Developer Desk" attr="desk">
{ship}/{foreignDesk}
</Attribute>
<Attribute title="Last Software Update" attr="case">
{moment(cass.da).format('YYYY.MM.DD')}
</Attribute>
<Attribute title="Desk Hash" attr="hash">
{hash}
</Attribute>
<Attribute title="Installed into" attr="local-desk">
%{desk}
</Attribute>
{next.length > 0 ? (
<Attribute attr="next" title="Pending Updates">
{next.length} update{pluralUpdates ? 's are' : ' is'} pending a System Update
</Attribute>
) : null}
</div>
);
}